接口
接口只是对一组方法签名进行了统一命名,这些方法不提供任何实现。类通过指定接口名称来继承接口,而且必须显示实现接口方法,否则CLR 会认为此类型定义无效。
类继承有一个重要的特点,凡是能使用基类型实例的地方,都能使用派生类型的实例。类似地,接口继承的一个重要特点是,凡是能使用具名接口的类型的实例的地方,都能使用实现了接口的一个类型的实例。
继承接口
C# 编译器要求将实现接口的方法标记为 public。CLR 要求将接口方法标记为virtual。
不将方法显示标记为 virtual,编译器会将它们标记为virtual和sealed:这会阻止派生类重写接口方法。
将方法显示标记为 virtual,编译器就会将该方法标记为virtual,使派生类能重写它。
派生类不能重写sealed 接口方法。但派生类可重新继承同一个接口,并为接口方法提供自己的实现。在对象上调用接口方法时,调用的是方法在该对象类型中的实现。
using System; public static class Program { public static void Main() { /************************* First Example *************************/ Base b = new Base(); // Calls Dispose by using b's type: "Base's Dispose" b.Dispose(); // Calls Dispose by using b's object's type: "Base's Dispose" ((IDisposable)b).Dispose(); /************************* Second Example ************************/ Derived d = new Derived(); // Calls Dispose by using d's type: "Derived's Dispose" d.Dispose(); // Calls Dispose by using d's object's type: "Derived's Dispose" ((IDisposable)d).Dispose();300 PART II Designing Types /************************* Third Example *************************/ b = new Derived(); // Calls Dispose by using b's type: "Base's Dispose" b.Dispose(); // Calls Dispose by using b's object's type: "Derived's Dispose" ((IDisposable)b).Dispose(); } } // This class is derived from Object and it implements IDisposable internal class Base : IDisposable { // This method is implicitly sealed and cannot be overridden public void Dispose() { Console.WriteLine("Base's Dispose"); } } // This class is derived from Base and it reimplements IDisposable internal class Derived : Base, IDisposable { // This method cannot override Base's Dispose. 'new' is used to indicate // that this method reimplements IDisposable's Dispose method new public void Dispose() { Console.WriteLine("Derived's Dispose"); // NOTE: The next line shows how to call a base class's implementation (if desired) // base.Dispose(); } }
隐式和显式接口方法实现(幕后发生的事情)
类型加载到CLR 中时,会为该类型创建并初始化一个方法表。在这个方法表中,类型引入的每个新方法都有对应的记录项:另外,还为该类型继承的所有虚方法添加了记录项。继承的虚方法既有继承层次结构中的各个基类型定义的,也有接口类型定义的。所以对于下面这个简单的类型定义:
internal sealed class SimpleType : IDisposable{
public void Dispose()
{
Console.WriteLine("Dispose");
}
}
该类型的方法表将包含以下方法的记录项。
-
object (隐式继承的基类)定义的所有虚实例方法。
-
IDisposable(继承的接口)定义的所有接口方法。本例只有一个方法,即Dispose ,因为IDisposable 接口只定义了这个方法。
-
SimpleType 引入的新方法 Dispose。
C# 编译器将新方法和接口方法匹配起来之后,会生成元数据,指明 SimpleType 类型的方法表中的两个记录项引用同一个实现。为了更清楚的理解这一点,下面的代码演示来了如何调用类的公共Dispose 方法以及如何调用 IDisposable 的Dispose 方法在类中的实现:
public sealed class Program{
public static void Main(){
SimpleType st = new SimpleType();
//调用公共的Dispose 方法实现,调用的是SimpleType定义的Dispose方法。
st.Dispose();
//调用IDisposable 的Dispose 方法的实现
IDisposable d = st;
d.Dispose();
}
}
由于C# 要求公共Dispose 方法同时是 IDisposable 的Dispose 方法的实现,所以会执行相同的代码。
现在重写SimpleType ,以便于看出区别:
internal sealed class SimpleType : IDisposable{
public void Dispose() {Console.WriteLine("public Dispose");}
void IDisposable.Dispose() {Console.WriteLine("IDisposable Dispose");}
}
在不改动前面的Main 方法的前提下,重新编译并再次运行程序,输出结果如下所示:
public Dispose
IDisposable Dispose
在C# 中,将定义方法的那个接口的名称作为方法名前缀(例如 IDisposable.Dispose),就会创建显示接口方法实现(Explicit Interface Method Implementation, EIMI)。注意,C#中不允许在定义显示接口方法时指定可访问性。编译器生成方法的元数据时,可访问性会自动设为private,防止其他代码在使用类的实例时直接调用接口方法。只有通过接口类型的变量才能调用接口的方法。
还要注意,EIMI 方法不能标记为 virtual ,所以不能被重写。这是由于 EIMI方法并非真的是类型的对象模型的一部分,他只是将接口和类型连接起来,同时避免公开行为/方法。
如果两个接口定义了具有相同名称和签名的方法。如下所示:
public interface IWindow{
Object GetMenu();
}
public interface IRestaurant{
Object GetMenu();
}
// This type is derived from System.Object and
// implements the IWindow and IRestaurant interfaces.
public sealed class MarioPizzeria : IWindow, IRestaurant {
// This is the implementation for IWindow's GetMenu method.
Object IWindow.GetMenu() { ... }
// This is the implementation for IRestaurant's GetMenu method.
Object IRestaurant.GetMenu() { ... }
// This (optional method) is a GetMenu method that has nothing
// to do with an interface.
public Object GetMenu() { ... }
}
由于这个类型实现多个接口的GetMenu方法,所以要告诉C# 编译器每个GetMenu方法对应的是哪个接口的实现。
MarioPizzeria mp = new MarioPizzeria();
// This line calls MarioPizzeria's public GetMenu method
mp.GetMenu();
// These lines call MarioPizzeria's IWindow.GetMenu method
IWindow window = mp;
window.GetMenu();
// These lines call MarioPizzeria's IRestaurant.GetMenu method
IRestaurant restaurant = mp;
restaurant.GetMenu()
小结:
-
接口于class 类似,但是它只为其成员提供了规格,而没有提供具体实现
-
接口的成员都是隐式抽象的,
-
接口的成员都是隐式public 的,不可以声明访问修饰符
-
实现接口对它的所有成员进行public实现
-
一个class 或者 struct 可以实现多个接口
-
对象和接口转换,可以隐式的把一个对象转换成它实现的接口
-
接口可以继承其他接口
-
实现多个接口时,可能造成方法签名的冲突,这时需要显示的继承接口
-
显示实现的方法不是public的,在调用时应该转成对应的接口再调用
-
-
隐式实现的接口成员默认是 sealed,不可继承
-
如果想进行重写的化,必须在基类中把成员标记为virtual 或者 abstract。
-
如果父类不标记virtual,子类会重写该方法,例如:
Child c = new Child();
c.Do(); // 调用的是子类实现的接口方法
((Parent)c).Do(); //这样写才会调用父类实现的方法(假如child 继承了Parent,Parent又实现了接口)
((IDo)c).Do(); //这样写还是会调用父类实现的方法,因为父类是对接口的直接实现 -
如果父类标记virtual,子类要标记override 重写该方法,还是上面的三个输出,会得到下面的结果:三个都是调用子类的方法。
Child c = new Child();
c.Do(); // 调用的是子类实现的接口方法
((Parent)c).Do(); //会调用子类实现的方法,相当于覆盖了父类的,父类的方法不存在了。
((IDo)c).Do(); //还是调用子类的实现方法
Parent p = new Parent();
p.Do();
((IFoo)p).Do(); //这里调用的是父类的方法
-
-
显示实现的接口成员不可以标记为virtual,也不可以通过寻常的方式来重写,但是可以对其重新实现。
-
子类可以重新实现父类已经实现的接口成员。
-
重新实现会"劫持" 成员的实现(通过转化为接口然后调用)
public class Parent: IDo
{
public void IDo.Do() => Console.WriteLine("Parent"); //显示实现
}
public class Child:Parent, IDo
{
public void Do() => Console.WriteLine("Child");
}
class Program
{
static void main(string[] args)
{
Child c = new Child();
c.Do();
((IDo)c).Do(); //这里调用子类方法
((Parent)c).Do(); //这里会报错
}
} -
-
重新实现接口的替代方案
-
隐式实现成员的时候,按需标记virtual
-
显示实现的成员的时候这样做:
public class TestBox : IUndoable
{
void IUndoable.Undo() => Undo();
protected virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox:TextBox
{
protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
} -
如果不想又子类,直接把class 给 sealed。
-
-
把struct 转化为接口会导致装箱,
-
调用struct 上隐式实现的成员不会导致装箱
interface I {void Foo();}
struct S : I { public void Foo() {}}
S s= new S();
s.Foo(); //不会装箱
I i = s;
i.Foo(); //会装箱 -