第 13 章 接口
第 13 章 接口
NyxX第 13 章 接口
本章内容:
- 类和接口继承
- 定义接口
- 继承接口
- 关于调用接口方法的更多探讨
- 隐式和显示接口方法实现(幕后发生的事情)
- 泛型接口
- 泛型和接口约束
- 实现多个具有相同方法名和签名的接口
- 用显式接口方法实现来增强编译时类型安全性
- 谨慎使用显式接口方法实现
- 设计:基类还是接口?
13.1 类和接口继承
由于 Microsoft 的开发团队已实现了 Object 的方法,所以从Object派生的任何类实际都继承了以下内容。
- 方法签名
使代码认为自己是在操作Object类的实例,但实际操作的可能是其他类的实例。 - 方法实现
使开发人员定义Object的派生类时不必手动实现Object的方法。
13.2 定义接口
如前所述,接口对一组方法签名进行了统一命名。注意,接口还能定义事件、无参属性和有参属性(C# 的索引器)。如前所述,所有这些东西本质上都是方法,它们只是语法上的简化。不过,接口不能定义任何构造器方法,也不能定义任何实例字段。
13.3 继承接口
C# 编译器要求将实现接口的方法(后文简称为“接口方法”)标记为 public。CLR 要求将接口方法标记为virtual。不将方法显式标记为virtual,编译器会将它们标记为virtual和sealed;这会阻止派生类重写接口方法。将方法显式标记为virtual,编译器就会将该方法标记为virtual(并保持它的非密封状态),使派生类能重写它。
派生类不能重写sealed的接口方法。但派生类可重新继承同一个接口,并为接口方法提供自己的实现。在对象上调用接口方法时,调用的是该方法在该对象的类型中的实现。下例对此进行了演示:
13.4 关于调用接口方法的更多探讨
CLR 允许定义接口类型的字段、参数或局部变量。使用接口类型的变量可以调用该接口定义的方法。此外,CLR 允许调用 Object 定义的方法,因为所有类都继承了 Object 的方法。
重要提示 和引用类型相似,值类型可实现零个或多个接口。但值类型的实例在转换为接口类型时必须装箱。这是由于接口变量是引用,必须指向堆上的对象,使 CLR 能检查对象的类型对象的类型对象指针,从而判断对象的确切类型。调用已装箱值类型的接口方法时,CLR 会跟随对象的类型对象指针找到类型对象的方法表,从而调用正确的方法。
13.5 隐式和显式接口方法实现(幕后发生的事情)
类型加载到 CLR 中时,会为该类型创建并初始化一个方法表(参见第 1 章“CLR的执行模型”)。在这个方法表中,类型引入的每个新方法都有对应的记录项;另外,还为该类型继承的所有虚方法添加了记录项。继承的虚方法既有继承层次结构中的各个基类型定义的,也有接口类型定义的。所以,对于下面这个简单的类型定义:
1 | internal sealed class SimpleType : IDisposable { |
类型的方法表将包含以下方法的记录项。
Object(隐式继承的基类)定义的所有虚实例方法。IDisposable(继承的接口)定义所有接口方法。本例只有一个方法,即Dispose,因为IDisposable接口只定义了这个方法。SimpleType引入的新方法Dispose。
C#编译器将新方法和接口方法匹配起来之后,会生成元数据,指明 SimpleType 类型的方法表中的两个记录项应引用同一个实现。
1 | public sealed class Program { |
1 | Dispose |
现在重写 SimpleType,以便于看出区别:
1 | internal sealed class SimpleType : IDisposable { |
1 | public Dispose |
在 C# 中,将定义方法的那个接口的名称作为方法名前缀(例如 IDisposable.Dispose),就会创建显式接口方法实现(Explicit Interface Method Implementation,EIMI①)。注意,C# 中不允许在定义显式接口方法时指定可访问性(比如 public或private)。但是,编译器生成方法的元数据时,可访问性会自动设为 private,防止其他代码在使用类的实例时直接调用接口方法。只有通过接口类型的变量才能调用接口方法。
还要注意,EIMI 方法不能标记为 virtual,所以不能被重写。这是用于 EIMI 方法并非真的是类型的对象模型的一部分
13.6 泛型接口
泛型接口的一些好处
- 泛型接口提供了出色的编译时类型安全性。
- 处理值类型时装箱次数会少很多。
- 类可以实现同一个接口若干次,只要每次使用不同的类型参数。
13.7 泛型和接口约束
将泛型类型参数约束为接口的好处
- 可将泛型类型参数约束为多个接口。这样一来,传递的参数的类型必须实现全部接口约束。
1 | // M 的类型参数 T 被约束为只支持同时实现了 |
- 传递值类型的实例时减少装箱。
C# 编译器为接口约束生成特殊 IL 指令,导致直接在值类型上调用接口方法而不装箱。
13.8 实现多个具有相同方法名和签名的接口
定义实现多个接口的类型时,这些接口可能定义了具有相同名称和签名的方法。要定义实现这两个接口的类型,必须使用“显式接口方法实现”来实现这个类型的成员,如下所示:
1 | private interface IWindow { |
代码在使用MarioPizzeria 对象时必须将其转换为具体的接口才能调用所需的方法。
13.9 用显式接口方法实现来增强编译时类型安全性
1 | internal struct SomeValueType : IComparable { |
经过这两处改动之后,就获得了编译时的类型安全性,而且不会发生装箱:
1 | public static void Main() { |
13.10 谨慎使用显式接口方法实现
EIMI 最主要的问题如下。
没有文档解释类型具体如何实现一个
EIMI方法,也没有Microsoft Visual Studio“智能感知”支持。值类型的实例在转换成接口时装箱。
EIMI不能由派生类型调用。
13.11 设计:基类还是接口
易用性
对于开发人员,定义从基类派生的新类型通常比实现接口的所有方法容易得多。基类型可提供大量功能,所以派生类型可能只需稍做改动。而提供接口的话,新类型必须实现所有成员。一致性实现
无论接口协定(contract)订立得有多好,都无法保证所有人百分之百正确实现它。而如果为基类型提供良好的默认实现,那么一开始得到的就是能正常工作并经过良好测试的类型。以后根据需要修改就可以了。版本控制
向基类型添加一个方法,派生类型将继承新方法。一开始使用的就是一个能正常工作的类型,用户的源代码甚至不需要重新编译。而向接口添加新成员,会强迫接口的继承者更改其源代码并重新编译。





