标记界面的目的是什么?
答案 0 :(得分:72)
根据“米奇小麦”的反应,这有点切线。
通常,每当我看到人们引用框架设计指南时,我总是想提到:
您通常应该在大多数情况下忽略框架设计指南。
这不是因为框架设计指南的任何问题。我认为.NET框架是一个很棒的类库。框架设计指南中有很多奇妙的东西。
但是,设计指南不适用于大多数程序员编写的大多数代码。他们的目的是创建一个由数百万开发人员使用的大型框架,而不是使图书馆编写更有效。
其中的许多建议可以指导您做以下事情:
.net框架很大,非常大。它是如此之大,以至于假设任何人都对它的每个方面都有详细的了解是绝对不合理的。事实上,假设大多数程序员经常遇到以前从未使用过的框架部分,这样更安全。
在这种情况下,API设计者的主要目标是:
框架设计指南促使开发人员创建实现这些目标的代码。
这意味着做一些事情,比如避免继承层,即使它意味着重复代码,或者将所有异常抛出代码推送到“入口点”而不是使用共享助手(这样堆栈跟踪在调试器中更有意义),还有很多其他类似的事情。
这些指南建议使用属性而不是标记接口的主要原因是删除标记接口使得类库的继承结构更加平易近人。与具有15种类型和2层次结构的类相比,具有30种类型和6层继承层次结构的类图非常令人生畏。
如果确实有数百万开发人员使用您的API,或者您的代码库非常大(例如超过100K LOC),那么遵循这些准则可以提供很多帮助。
如果500万开发者花费15分钟学习API而不是花费60分钟学习它,结果是净节省了428个人年。那是很多时间。
然而,大多数项目不涉及数百万开发人员或100K + LOC。在一个典型的项目中,有4个开发人员和大约50K loc,这组假设有很大不同。团队中的开发人员将更好地理解代码的工作原理。这意味着优化快速生成高质量代码,减少错误数量和进行更改所需的工作量更有意义。
花费1周开发与.net框架一致的代码,而8小时编写易于更改且错误较少的代码可能会导致:
如果没有4,999,999名其他开发人员承担这些费用通常是不值得的。
例如,对标记接口的测试归结为单个“is”表达式,导致查找属性的代码更少。
所以我的建议是:
答案 1 :(得分:43)
标记接口用于将类的功能标记为在运行时实现特定接口。
Interface Design和.NET Type Design Guidelines - Interface Design不鼓励使用标记接口来支持在C#中使用属性,但正如@Jay Bazuzi指出的那样,检查标记接口比检查属性更容易:{ {1}}
所以不要这样:
o is I
.NET指南建议您这样做:
public interface IFooAssignable {}
public class FooAssignableAttribute : IFooAssignable
{
...
}
答案 2 :(得分:20)
由于其他每一个答案都说“应该避免它们”,因此解释原因会很有用。
首先,为什么要使用标记接口:它们的存在是为了允许使用实现它的对象的代码检查它们是否实现了所述接口,如果有,则以不同方式处理对象。
这种方法的问题在于它破坏了封装。对象本身现在可以间接控制外部使用方式。此外,它了解将要使用的系统。通过应用标记接口,类定义表明它希望用于检查标记存在的某个地方。它隐含地了解它所使用的环境,并试图定义它应该如何被使用。这违背了封装的想法,因为它了解完全在其自身范围之外存在的系统的一部分的实现。
在实际水平上,这降低了可移植性和可重用性。如果在不同的应用程序中重用该类,则需要同时复制该接口,并且在新环境中它可能没有任何意义,使其完全冗余。
因此,“标记”是关于类的元数据。这个元数据不是由类本身使用,只对(某些!)外部客户端代码有意义,因此它可以以某种方式处理对象。因为它只对客户端代码有意义,所以元数据应该在客户端代码中,而不是类API。
“标记接口”和普通接口之间的区别在于具有方法的接口告诉外界如何使用 ,而空接口意味着它告诉外界它是如何<应该使用。
答案 3 :(得分:7)
当语言不支持歧视联合类型时,标记界面有时可能是必要的恶魔。
假设您要定义一个方法,该方法需要一个类型必须恰好是A,B或C之一的参数。在许多功能优先的语言(如F#)中,可以干净地定义这样的类型为:
type Arg =
| AArg of A
| BArg of B
| CArg of C
但是,在诸如C#的OO优先语言中,这是不可能的。在这里实现类似的东西的唯一方法是定义接口IArg和&#34;标记&#34; A,B和C用它。
当然,您可以通过简单地接受类型&#34; object&#34;来避免使用标记界面。作为论点,但你会失去表现力和某种程度的类型安全。
歧视的联合类型非常有用,并且已经存在于函数式语言中至少30年。奇怪的是,直到今天,所有主流OO语言都忽略了这一功能 - 尽管它本身与功能编程无关,但属于类型系统。
答案 4 :(得分:6)
标记接口只是一个空的接口。类会将此接口实现为由于某种原因而使用的元数据。在C#中,您更常使用属性来标记类,原因与您在其他语言中使用标记接口的原因相同。
答案 5 :(得分:4)
标记接口允许以适用于所有后代类的方式标记类。 “纯”标记接口不会定义或继承任何东西;更有用的标记接口类型可以是“继承”另一个接口但不定义新成员的接口。例如,如果存在接口“IReadableFoo”,则可能还会定义一个接口“IImmutableFoo”,其行为类似于“Foo”,但会保证使用它的任何人都不会改变其值。接受IImmutableFoo的例程将能够像使用IReadableFoo一样使用它,但例程只接受声明为实现IImmutableFoo的类。
我无法想到“纯”标记接口的大量用法。我能想到的唯一一个是,如果EqualityComparer(T).Default会为实现IDoNotUseEqualityComparer的任何类型返回Object.Equals,即使该类型也实现了IEqualityComparer。这将允许一个具有未密封的不可变类型而不违反Liskov替换原则:如果类型密封所有与相等测试相关的方法,派生类型可以添加额外的字段并使它们是可变的,但这些字段的变异不会使用任何基本类型方法都可以看到。拥有一个未密封的不可变类并且避免使用EqualityComparer.Default或信任派生类不实现IEqualityComparer可能并不可怕,但是实现IEqualityComparer的派生类即使被视为基类,也可能看起来像一个可变类。类对象。
答案 6 :(得分:4)
这两种扩展方法将解决Scott声称支持标记接口的大部分问题:
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)
{
//...
}
我没有看到如第一个模式与第二个模式相比,构建一个API需要花费5倍的时间,正如Scott声称的那样。
答案 7 :(得分:1)
标记器界面实际上只是一种用OO语言编程的程序。 除标记接口外,接口定义了实现者和使用者之间的契约,因为标记接口仅定义了自身。因此,标记器接口一经出现就以成为接口为基本目的而失败。
答案 8 :(得分:0)
标记是空接口。标记是存在还是不存在。
Foo类:IConfidential
这里我们将Foo标记为机密。不需要实际的附加属性或属性。
答案 9 :(得分:0)
标记界面是一个完整的空白界面,没有正文/数据成员/实现 一个类在需要时实现标记接口,它只是为了&#34; 标记&#34 ;;意味着它告诉JVM特定类是为了克隆的目的,所以允许它克隆。这个特殊的类是序列化它的对象所以请允许它的对象被序列化。