《CLR via C#》 接口

接口实际只是对一组方法签名进行了统一命名。这些方法不提供任何实现。类通过指定接口名称来继承接口,而且必须显式实现接口方法,否则CLR会认为此类型定义无效。

定义接口

接口可以定义事件、无参属性和有参属性。但接口不能定义任何构造器方法,也布恩那个定义任何实例字段。

提示

CLR允许接口定义静态方法、静态字段、常量和静态构造器,但C#不允许。

注意

对于CLR来说接口定义就是类型定义。

继承接口

C#编译器要求将实现接口的方法(接口方法)标记为public。CLR要求将接口方法标记为virtual。不将方法标记为virtual,编译器会将他们标记为virtual和sealed;这会阻止派生类重写接口方法。将方法显式标记为virtual,编译器就会将该方法标记为virtual(并保持他的非密封状态),使派生类能重写它。

派生类不能重写sealed接口方法。但派生类可以重新继承同一个接口,并为接口方法提供自己的实现。在对象上调用接口方法时,调用的是该方法在该对象的类型中的实现

 1// 这个类派生自Object,它实现了IDisposable
 2internal class Base : IDisposable {
 3    // 这个方法隐式密封,不能被重写
 4    public void Dispose() {
 5        Console.WriteLine("Base's Dispose");
 6    }
 7}
 8
 9// 这个类派生自Base,它重新实现了IDisposable
10internal class Derived : Base, IDisposable {
11    // 这个方法不能重写Base的Dispose,'new'表明该方法重新实现了IDisposable的Dispose方法
12    new public void Dispose() {
13        Console.WriteLine("Derived's Dispose");
14        // 调用基类的实现
15        base.Dispose();
16    }
17}
18
19Base b = new Base();
20b.Dispose();                // 显示 "Base's Dispose"
21((IDisposable)b).Dispose(); // 显示 "Base's Dispose"
22
23Derived d = new Derived();
24d.Dispose();                // 显示 "Derived's Dispose"
25((IDisposable)d).Dispose(); // 显示 "Derived's Dispose"
26
27b = new Derived();
28b.Dispose();                // 显示 "Base's Dispose"
29((IDisposable)b).Dispose(); // 显示 "Derived's Dispose"

注意

值类型可以实现零个或多个接口,但值类型的实例在转换为接口类型的时候必须装箱。

隐式和显式接口方法实现

类型加载到CLR中会为类型创建一个方法表。这个表中类型引入的每个新方法都有对应的记录项;另外,还为该类型继承的所有虚方法添加了记录项。继承的虚方法既有继承层次就中各个基类型定义的,也有接口定义的。

1internal sealed class SimpleType : IDisposable {
2    public void Dispose() {Console.WriteLine("Dispose");}
3}
4
5SimpleType st = new SimpleType();
6st.Dispose(); // 输出:"Dispose"
7IDisposable d = st;
8d.Dispose();  // 输出:"Dispose"

上述代码中类SimpleType的方法表包含:

  • Object(隐式继承)定义的所有虚实例方法。
  • IDisposable(继承的接口)定义的所有接口方法。
  • SimpleType引入的新方法Dispose。

IDisposable中有Dispose方法,但是方法表中将其与SimpleType的Dispose方法区分开来。但C#编译器假定SimpleType中的Dispose方法是对IDisposable的Dispose方法的实现,因为两个方法的签名(可访问型,名称等)完全一致。如果Dispose方法被标记为virtual,C#编译器也认为其方法匹配接口

 1internal sealed class SimpleType : IDisposable {
 2    public void Dispose() {Console.WriteLine("Dispose");}
 3    // 显式实现接口方法(不允许指定可访问性,默认为private),不能标记为virtual
 4    void IDisposable.Dispose() {Console.WriteLine("IDisposable.Dispose");}
 5}
 6
 7SimpleType st = new SimpleType();
 8st.Dispose(); // 输出:"Dispose"
 9IDisposable d = st;
10d.Dispose();  // 输出:"IDisposable.Dispose"

上述方法中将定义方法接口名称作为方法名前缀的写法就为显式接口方法实现(EIMI)。C#不允许指定可访问性,自动设为private,为防止其他代码在使用类的实例时直接调用接口方法。只有通过接口类型的变量才能调用接口方法。

注意

EIMI不能标记为virtual,所以不能被重写。由于EIMI方法并非真的是类型的对象模型的一部分,它只是将接口和类型连接起来,同时避免公开行为/方法

泛型接口

好处:

  • 提供编译时类型安全性
  • 装箱变少
  • 类可以实现同一个接口若干次,只需要每次使用不同的参数类型

泛型和接口约束

将泛型参数约束为接口的好处

  • 可将放行类型参数约束为多个接口,这样一来,传递的参数类型必须实现全部接口类型。
    • 如果参数的类型时接口,那么实参可以时任意类型,只要该类实现了接口。
  • 传递值类型的实例时减少装箱。
    • C#编译器为接口约束生成特殊IL指令,导致直接在值类型上调用接口方法而不装箱。

注意

在值类型实现了一个接口方法,在值类型的实例上调用这个方法不会装箱。

实现多个具有相同方法名和签名的接口

继承的多个接口具有相同方法名和返回值时,必须使用“显式接口方法实现”来实现这个方法。

 1public interface IWindow {
 2    Object GetMenu();
 3}
 4
 5public interface IRestaurant {
 6    Object GetMenu();
 7}
 8
 9public sealed class A : IWindow, IRestaurant {
10    // IWindow接口的实现
11    Object IWindow.GetMenu() { ... }
12    // IRestaurant接口的实现
13    Object IRestaurant.GetMenu() { ... }
14}

在调用其对象时,必须将其转换胃具体的接口才能调用所需的方法。

1A a = new A();
2
3IWindow window = a;
4window.GetMenu(); // 调用的IWindow接口的方法
5
6IRestaurant restaurant = a;
7resturant.GetMenu(); // 调用IRestaurant接口的方法

谨慎使用EIMI

EIMI的问题

  • 值类型的实例在转换成接口时装箱
  • EIMI不能由派生类调用