【发布时间】:2010-11-04 14:47:35
【问题描述】:
标记界面的用途是什么?
【问题讨论】:
标签: c# interface class-design
标记界面的用途是什么?
【问题讨论】:
标签: c# interface class-design
根据“米奇小麦”的回应,这有点切线。
一般来说,每当我看到人们引用框架设计指南时,我总是喜欢提到:
您通常应该在大多数情况下忽略框架设计指南。
这不是因为框架设计指南有任何问题。我认为 .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”表达式,并导致查找属性的代码更少。
所以我的建议是:
【讨论】:
virtual protected 模板方法 DoSomethingCore 而不是 DoSomething 并没有太多额外的工作,您清楚地传达它是一个模板方法... IMNSHO,编写应用程序而不考虑 API 的人(@987654324 @) 正是那些编写大量重复(并且也没有文档且通常不可读)代码的人,而不是相反。
标记接口用于将类的能力标记为在运行时实现特定接口。
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
{
...
}
【讨论】:
由于所有其他答案都表明“应该避免使用它们”,因此解释一下原因会很有用。
首先,为什么使用标记接口:它们的存在是为了允许使用实现它的对象的代码检查它们是否实现了所述接口,如果实现了,则以不同的方式对待对象。
这种方法的问题在于它破坏了封装。对象本身现在可以间接控制它在外部的使用方式。此外,它了解将要使用的系统。通过应用标记接口,类定义表明它希望在某个地方使用它来检查标记的存在。它对它所使用的环境有隐含的了解,并试图定义它应该如何被使用。这违背了封装的想法,因为它知道系统的一部分完全存在于它自己的范围之外的实现。
实际上,这会降低可移植性和可重用性。如果该类在不同的应用程序中重用,则该接口也需要复制,并且它在新环境中可能没有任何意义,使其完全冗余。
因此,“标记”是关于类的元数据。此元数据不被类本身使用,仅对(某些!)外部客户端代码有意义,以便它可以以某种方式处理对象。因为它只对客户端代码有意义,所以元数据应该在客户端代码中,而不是类 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>,您在接口中有一个描述某些行为的方法,因此它不是标记接口。
ProduceFromString 不会以任何方式涉及接口,只是接口将用作标记来指示应该期望哪些类来实现必要的功能。
当一种语言不支持可区分联合类型时,标记接口有时可能是必要的。
假设您要定义一个方法,该方法需要一个类型必须恰好是 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 使用通用接口约束将在编译器时检查提供的类型是否至少声称是有效的,即使它无法确保它实际上是有效的。
标记接口只是一个空的接口。由于某种原因,一个类会将此接口实现为要使用的元数据。在 C# 中,您更常使用属性来标记类,原因与在其他语言中使用标记接口的原因相同。
【讨论】:
这两种扩展方法将解决 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 倍。
【讨论】:
标记接口允许以将应用于所有后代类的方式标记类。 “纯”标记接口不会定义或继承任何东西;一种更有用的标记接口类型可能是“继承”另一个接口但不定义新成员的标记接口。例如,如果有一个接口“IReadableFoo”,也可以定义一个接口“IImmutableFoo”,它的行为类似于“Foo”,但会向任何使用它的人保证不会改变它的值。接受 IImmutableFoo 的例程可以像使用 IReadableFoo 一样使用它,但例程只接受声明为实现 IImmutableFoo 的类。
我想不出“纯”标记界面有很多用途。我能想到的唯一一个是 EqualityComparer(of T).Default 是否会为任何实现 IDoNotUseEqualityComparer 的类型返回 Object.Equals,即使该类型也实现了 IEqualityComparer。这将允许一个人在不违反 Liskov 替换原则的情况下拥有一个未密封的不可变类型:如果该类型密封了所有与相等性测试相关的方法,则派生类型可以添加额外的字段并使它们是可变的,但这些字段的突变不会t 使用任何基本类型的方法都是可见的。拥有一个未密封的不可变类并避免使用 EqualityComparer.Default 或信任派生类不实现 IEqualityComparer 可能并不可怕,但是实现 IEqualityComparer 的派生类即使被视为基类也可能显示为可变类 -类对象。
【讨论】:
标记是空接口。标记要么存在,要么不存在。
类 Foo : IConfidential
在这里,我们将 Foo 标记为机密。不需要实际的附加属性或属性。
【讨论】:
标记界面实际上只是一种面向对象语言的过程式编程。 接口定义了实现者和消费者之间的契约,除了标记接口,因为标记接口只定义了它自己。因此,从一开始,标记接口就无法实现作为接口的基本目的。
【讨论】:
标记接口是一个完全空白的接口,没有正文/数据成员/实现。
一个类实现需要的时候标记接口,只是为了“mark”;意味着它告诉 JVM 特定类是为了克隆,所以允许它克隆。这个特定的类要序列化它的对象,所以请允许它的对象被序列化。
【讨论】: