标记界面的目的是啥?
Posted
技术标签:
【中文标题】标记界面的目的是啥?【英文标题】:What is the purpose of a marker interface?标记界面的目的是什么? 【发布时间】:2010-11-04 14:47:35 【问题描述】:标记界面的用途是什么?
【问题讨论】:
【参考方案1】:根据“米奇小麦”的回应,这有点切线。
一般来说,每当我看到人们引用框架设计指南时,我总是喜欢提到:
您通常应该在大多数情况下忽略框架设计指南。
这不是因为框架设计指南有任何问题。我认为 .NET 框架是一个很棒的类库。很多奇妙的东西来自框架设计指南。
但是,设计指南不适用于大多数程序员编写的大多数代码。他们的目的是创建一个供数百万开发人员使用的大型框架,而不是提高库编写效率。
其中的许多建议可以指导您做以下事情:
-
可能不是最直接的实现方式
可能会导致额外的代码重复
可能有额外的运行时开销
.net 框架很大,真的很大。它是如此之大,以至于假设任何人都对它的各个方面都有详细的了解是绝对不合理的。事实上,假设大多数程序员经常遇到他们以前从未使用过的框架部分,这会更安全。
在这种情况下,API 设计人员的主要目标是:
-
与框架的其余部分保持一致
消除 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”表达式,并导致查找属性的代码更少。
所以我的建议是:
-
如果您正在开发旨在广泛使用的类库(或 UI 小部件),请认真遵循框架指南。
如果您的项目中有超过 100K LOC,请考虑采用其中的一些
否则完全忽略它们。
【讨论】:
我个人认为,我编写的任何代码都是我以后需要使用的库。我并不真正关心消费是否普遍 - 遵循准则可以提高一致性,并在多年后需要查看我的代码并理解它时减少意外...... 我并不是说指南不好。我是说它们应该有所不同,具体取决于您的代码库的大小和您拥有的用户数量。许多设计指南都基于诸如维护二进制可比性之类的东西,这对于少数项目使用的“内部”库并不像对 BCL 这样的东西那么重要。其他指南,例如与可用性相关的指南,几乎总是很重要。道德是不要对指南过于虔诚,尤其是在小型项目上。 +1 - 没有完全回答 OP 的问题 - MI 的目的 - 但非常有帮助。 @ScottWisniewski:我认为你错过了严重的观点。框架指南不适用于大型项目,它们适用于中型和一些小型项目。当您总是尝试将它们应用于 Hello-World 程序时,它们会变得过度杀伤力。例如,将接口限制为 5 个方法始终是一个很好的经验法则,无论应用程序大小如何。你错过的另一件事是,今天的小应用程序可能会成为明天的大应用程序。因此,最好在构建时考虑到适用于大型应用程序的良好原则,这样当需要扩展时,您不必重新编写大量代码。 我不太明白遵循(大部分)设计指南会如何导致一个 8 小时的项目突然耗时 1 周。例如:命名virtual protected
模板方法 DoSomethingCore
而不是 DoSomething
并没有太多额外的工作,您清楚地传达它是一个模板方法... IMNSHO,编写应用程序而不考虑 API 的人(@987654324 @) 正是那些编写大量重复(并且也没有文档且通常不可读)代码的人,而不是相反。【参考方案2】:
标记接口用于将类的能力标记为在运行时实现特定接口。
Interface Design 和 .NET Type Design Guidelines - Interface Design 不鼓励使用标记接口,而是支持在 C# 中使用属性,但正如 @Jay Bazuzi 指出的那样,检查标记接口比检查属性更容易:o is I
所以不要这样:
public interface IFooAssignable
public class Foo : IFooAssignable
...
.NET 指南建议您这样做:
public class FooAssignableAttribute : Attribute
...
[FooAssignable]
public class Foo
...
【讨论】:
另外,我们可以将泛型与标记接口完全结合使用,但不能与属性结合使用。 虽然我喜欢属性以及从声明的角度来看它们的外观,但它们在运行时并不是一等公民,需要大量相对低级的管道才能使用。 @Jordão - 这正是我的想法。举个例子,如果我想抽象数据库访问代码(比如 Linq to Sql),那么拥有一个通用接口会变得容易得多。事实上,我认为不可能用属性编写这种抽象,因为您不能强制转换为属性,也不能在泛型中使用它们。我想您可以使用一个空基类,其他类都从该基类派生,但这感觉或多或少与拥有一个空接口相同。另外,如果您后来意识到需要共享功能,则该机制已经到位。【参考方案3】:由于所有其他答案都表明“应该避免使用它们”,因此解释一下原因会很有用。
首先,为什么使用标记接口:它们的存在是为了允许使用实现它的对象的代码检查它们是否实现了所述接口,如果实现了,则以不同的方式对待对象。
这种方法的问题在于它破坏了封装。对象本身现在可以间接控制它在外部的使用方式。此外,它了解将要使用的系统。通过应用标记接口,类定义表明它希望在某个地方使用它来检查标记的存在。它对它所使用的环境有隐含的了解,并试图定义它应该如何被使用。这违背了封装的想法,因为它知道系统的一部分完全存在于它自己的范围之外的实现。
实际上,这会降低可移植性和可重用性。如果该类在不同的应用程序中重用,则该接口也需要复制,并且它在新环境中可能没有任何意义,使其完全冗余。
因此,“标记”是关于类的元数据。此元数据不被类本身使用,仅对(某些!)外部客户端代码有意义,以便它可以以某种方式处理对象。因为它只对客户端代码有意义,所以元数据应该在客户端代码中,而不是类 API。
“标记接口”和普通接口之间的区别在于,带有方法的接口告诉外界它如何使用,而空接口则意味着它告诉外界它如何使用 em>应该使用。
【讨论】:
任何接口的主要目的是区分承诺遵守与该接口相关的契约的类和不遵守的类。虽然接口还负责提供履行合同所需的任何成员的调用签名,但决定特定接口是否应由特定类实现的是合同,而不是成员。如果IConstructableFromString<T>
的合同规定类T
只能实现IConstructableFromString<T>
,如果它有一个静态成员......
...public static T ProduceFromString(String params);
,接口的伴生类可以提供方法public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>
;如果客户端代码有像T[] MakeManyThings<T>() where T:IConstructableFromString<T>
这样的方法,则可以定义可以与客户端代码一起使用的新类型,而无需修改客户端代码来处理它们。如果元数据在客户端代码中,则无法创建新类型供现有客户端使用。
但是T
和使用它的类之间的约定是IConstructableFromString<T>
,您在接口中有一个描述某些行为的方法,因此它不是标记接口。
类必须具有的静态方法不是接口的一部分。接口中的静态成员由接口自己实现;接口无法引用实现类中的静态成员。
一个方法可以通过Reflection来判断一个泛型类型是否碰巧有一个特定的静态方法,如果存在就执行那个方法,但是搜索和执行静态方法的实际过程上面示例中的方法ProduceFromString
不会以任何方式涉及接口,只是接口将用作标记来指示应该期望哪些类来实现必要的功能。【参考方案4】:
当一种语言不支持可区分联合类型时,标记接口有时可能是必要的。
假设您要定义一个方法,该方法需要一个类型必须恰好是 A、B 或 C 之一的参数。在许多功能优先语言(如F#)中,这种类型可以明确定义为:
type Arg =
| AArg of A
| BArg of B
| CArg of C
但是,在 C# 等面向对象优先的语言中,这是不可能的。在这里实现类似功能的唯一方法是定义接口 IArg 并用它“标记”A、B 和 C。
当然,您可以通过简单地接受类型“object”作为参数来避免使用标记接口,但这样会失去表达能力和某种程度的类型安全性。
可区分联合类型非常有用,并且在函数式语言中已经存在了至少 30 年。奇怪的是,直到今天,所有主流的 OO 语言都忽略了这个特性——虽然它实际上与函数式编程本身无关,而是属于类型系统。
【讨论】:
值得注意的是,因为Foo<T>
将为每个类型 T
提供一组单独的静态字段,所以不难让泛型类包含包含代表的静态字段来处理 @987654325 @,并使用函数预先填充这些字段以处理该类应该使用的每种类型。对类型 T
使用通用接口约束将在编译器时检查提供的类型是否至少声称是有效的,即使它无法确保它实际上是有效的。【参考方案5】:
标记接口只是一个空的接口。由于某种原因,一个类会将此接口实现为要使用的元数据。在 C# 中,您更常使用属性来标记类,原因与在其他语言中使用标记接口的原因相同。
【讨论】:
【参考方案6】:这两种扩展方法将解决 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)
//...
Scott 声称,与第二种模式相比,第一种模式构建 API 所需的时间是第二种模式的 5 倍。
【讨论】:
仍然没有泛型。【参考方案7】:标记接口允许以将应用于所有后代类的方式标记类。 “纯”标记接口不会定义或继承任何东西;一种更有用的标记接口类型可能是“继承”另一个接口但不定义新成员的标记接口。例如,如果有一个接口“IReadableFoo”,也可以定义一个接口“IImmutableFoo”,它的行为类似于“Foo”,但会向任何使用它的人保证不会改变它的值。接受 IImmutableFoo 的例程可以像使用 IReadableFoo 一样使用它,但例程只接受声明为实现 IImmutableFoo 的类。
我想不出“纯”标记界面有很多用途。我能想到的唯一一个是 EqualityComparer(of T).Default 是否会为任何实现 IDoNotUseEqualityComparer 的类型返回 Object.Equals,即使该类型也实现了 IEqualityComparer。这将允许一个人在不违反 Liskov 替换原则的情况下拥有一个未密封的不可变类型:如果该类型密封了所有与相等性测试相关的方法,则派生类型可以添加额外的字段并使它们是可变的,但这些字段的突变不会t 使用任何基本类型的方法都是可见的。拥有一个未密封的不可变类并避免使用 EqualityComparer.Default 或信任派生类不实现 IEqualityComparer 可能并不可怕,但是实现 IEqualityComparer 的派生类即使被视为基类也可能显示为可变类 -类对象。
【讨论】:
【参考方案8】:标记是空接口。标记要么存在,要么不存在。
类 Foo : IConfidential
在这里,我们将 Foo 标记为机密。不需要实际的附加属性或属性。
【讨论】:
【参考方案9】:标记界面实际上只是一种面向对象语言的过程式编程。 接口定义了实现者和消费者之间的契约,除了标记接口,因为标记接口只定义了它自己。因此,从一开始,标记接口就无法实现作为接口的基本目的。
【讨论】:
【参考方案10】:标记接口是一个完全空白的接口,没有正文/数据成员/实现。 一个类实现需要的时候标记接口,只是为了“mark”;意味着它告诉 JVM 特定类是为了克隆,所以允许它克隆。这个特定的类要序列化它的对象,所以请允许它的对象被序列化。
【讨论】:
以上是关于标记界面的目的是啥?的主要内容,如果未能解决你的问题,请参考以下文章