标记接口的目的是什么?
这是基于"Mitch Wheat"回答的一个有点离题的观点。
一般来说,每当我看到人们引用框架设计指南时,我总是喜欢提醒大家:
在大多数情况下,你应该忽略框架设计指南。
这并不是因为框架设计指南存在任何问题。我认为.NET框架是一个非常出色的类库。其中很多精彩之处都来自于框架设计指南。
然而,设计指南并不适用于大多数程序员编写的大多数代码。它们的目的是为了创建一个被数百万开发者使用的大型框架,而不是为了使库的编写更加高效。
它中提到的许多建议可能会引导你做出以下结果:
.net框架非常庞大。它如此之大,以至于认为任何人对其各个方面都有详细的了解是绝对不合理的。事实上,假设大多数程序员经常遇到他们从未使用过的框架部分更加安全。
在这种情况下,API设计者的主要目标是:
框架设计指南推动开发人员创造实现这些目标的代码。
这意味着采取措施避免多层继承,即使这意味着重复编写代码,或将所有异常抛出代码推向“入口点”,而不是使用共享帮助程序(以便堆栈跟踪在调试器中更有意义),以及许多类似的事情。
这些指南建议使用属性而不是标记接口的主要原因是因为删除标记接口会使类库的继承结构更易于理解。具有30个类型和6层继承层次结构的类图比具有15个类型和2层层次结构的类图更加令人望而却步。
如果有数百万开发人员使用您的API,或者您的代码库非常庞大(比如超过100K LOC),那么遵循这些准则可以帮助很多。标记接口用于在运行时标记类实现特定接口的能力。
接口设计和.NET类型设计准则-接口设计推荐在C#中使用属性,而不是标记接口。但正如@Jay Bazuzi指出的那样,检查标记接口比检查属性更容易:o is I
所以,代替这种方式:
public interface IFooAssignable {}
public class Foo : IFooAssignable
{
...
}
根据.NET指南,建议您这样做:
public class FooAssignableAttribute : Attribute
{
...
}
[FooAssignable]
public class Foo
{
...
}
因为其他回答都表明“应该避免使用”,所以有必要解释一下为什么。
首先,为什么会使用标记接口:它们存在的目的是允许使用实现此接口的对象的代码检查它们是否实现了该接口,并在其实现了该接口时以不同的方式处理该对象。
这种方法的问题在于它破坏了封装性。对象本身现在对如何在外部使用它具有间接控制权。此外,它还知道它将被用于的系统。通过应用标记接口,类定义暗示它期望在某个检查标记存在的地方使用。它隐含地知道它所用于的环境,并试图定义它应该如何被使用。这与封装思想相违背,因为它知道完全超出自己范围的系统部分的实现细节。
从实际角度来看,这会减少可移植性和可重用性。如果在不同的应用程序中重新使用该类,则还需要复制接口,而在新环境中可能没有任何意义,使其完全无用。
因此,“标记”是关于类的元数据。这些元数据不会被类本身使用,只有对(某些)外部客户端代码有意义,以便它可以以某种方式处理对象。因为这些元数据仅对客户端代码有意义,所以它们应该在客户端代码中,而不是类API中。
“标记接口”和普通接口之间的区别在于,具有方法的接口告诉外部世界它如何使用,而空接口则意味着它在告诉外部世界它应该如何被使用。
IConstructableFromString<T>
的契约规定只有具有静态成员的类T
才能实现IConstructableFromString<T>
... - supercatpublic static T ProduceFromString(String params);
,该接口的伴随类可以提供一个方法public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>
;如果客户端代码有一个像T[] MakeManyThings<T>() where T:IConstructableFromString<T>
这样的方法,就可以定义新类型,使其能够与客户端代码一起使用,而无需修改客户端代码以处理它们。如果元数据在客户端代码中,则无法创建新类型以供现有客户端使用。 - supercatT
和使用它的类之间的契约是IConstructableFromString<T>
,其中接口中有一个描述某些行为的方法,因此它不是标记接口。 - Tom BProduceFromString
的实际过程不会以任何方式涉及接口,除了使用接口作为标记来指示应该期望哪些类实现必要的函数。 - supercat当语言不支持判别式联合类型时,标记接口有时可能是必要的恶。
假设您想定义一个方法,该方法期望参数的类型必须是 A、B 或 C 中的一种。在许多以函数为先的语言(如 F#)中,这样的类型可以干净地定义为:
type Arg =
| AArg of A
| BArg of B
| CArg of C
Foo<T>
对于每个类型 T
都有一个单独的静态字段集,所以很容易让泛型类包含静态字段,这些字段包含处理 T
的委托,并使用函数预先填充这些字段以处理类应该处理的每种类型。在类型 T
上使用泛型接口约束将在编译时检查提供的类型至少声称是有效的,尽管它无法确保它实际上是有效的。 - supercat标记接口就是一个空接口。一个类实现这个接口作为元数据,用于某些目的。在C#中,您通常会使用属性来标记一个类,以达到在其他语言中使用标记接口的同样目的。
public static bool HasAttribute<T>(this ICustomAttributeProvider self)
where T : Attribute
{
return self.GetCustomAttributes(true).Any(o => o is T);
}
public static bool HasAttribute<T>(this object self)
where T : Attribute
{
return self != null && self.GetType().HasAttribute<T>()
}
现在你拥有:
if (o.HasAttribute<FooAssignableAttribute>())
{
//...
}
对比:
if (o is IFooAssignable)
{
//...
}
我无法理解为什么按照Scott所说的,使用第一种模式构建API需要比第二种模式花费5倍的时间。
标记是空接口。标记要么存在,要么不存在。
类 Foo : IConfidential
在这里,我们将 Foo 标记为机密。不需要实际的其他属性或属性。