# 原则23:理解接口方法和虚函数的区别
**By D.S.Qiu**
**尊重他人的劳动,支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
咋一看,实现接口和重载一个虚函数似乎是一样的。都是定义一个在另一个类中声明的成员。第一眼的感觉是很有欺骗性的。实现接口和重载虚函数是非常不同的。在接口声明的成员是非虚的——至少不是默认的。子类不能重载基类实现的接口的成员。接口可以显示实现,可以把它们中 public 接口中隐藏。它们的概念不同而且使用也不同。
但是你可以这样实现接口以至于子类可以修改你的实现。你只需要对子类做一个 hook 就行了。
为了说明它们的不同,定义一个简单的几块和它的实现类:
```
interface IMsg
{
void Message();
}
public class MyClass : IMsg
{
public void Message()
{
Console.WriteLine("MyClass");
}
}
```
Message() 方法是 MyClass 类公有接口的一部分。Message 也可以通过 IMsg 指针访问,它是 MyClass 类型的一部分。现在通过添加子类变得更复杂:
```
public class MyDerivedClass : MyClass
{
public void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
```
注意到我不得不添加 new 关键字用以区别之前的 Message 方法(查看原则33)(译者注:这应该是第一版的叙述)。 MyClass.Message() 是非虚的。子类不能提供重载的 Message 版本。 MyClass 创建了新的 Message 方法,但是这个方法没有重载 MyClass.Message : 它会被隐藏。更重要的是,MyClass.Message 仍然可通过 IMsg 引用访问:
```
MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass".
IMsg m = d as IMsg;
m.Message(); // prints "MyClass"
```
接口的方法是非虚的。当你实现接口,你就在这个类中声明这个合约的具体的实现。
但是你经常想要创建接口,在基类实现它们,并且在子类修改它们的行为。你确实可以做到。你有两种选择。要是你不能接触到基类,你可以在子类中重新实现接口:
```
public class MyDerivedClass : MyClass
{
public new void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
```
新增的关键字使得 IMsg 改变行为子类的行为以至于 IMsge.Message 可以调用子类的版本:
```
MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass".
IMsg m = d as IMsg;
m.Message(); // prints " MyDerivedClass "
```
如果你仍然使用 new 关键字在 MyDerivedClass.Message 方法上。给你个提示:仍然还会有问题(查看原则33)。子类的版本仍然可以通过子类的引用访问到:
```
MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass".
IMsg m = d as IMsg;
m.Message(); // prints "MyDerivedClass"
MyClass b = d;
b.Message(); // prints "MyClass"
```
修复这个问题方法是修改基类,声明接口方法为 virtual :
```
public class MyClass : IMsg
{
public virtual void Message()
{
Console.WriteLine("MyClass");
}
}
public class MyDerivedClass : MyClass
{
public override void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
```
MyDerivedClass——其他所有继承自 MyClass ——都可以声明它们自己的 Message() 方法。重载的版本总是会被调用:无论是通过 MyDerivedClass 引用,还是通过 IMsg 引用,或者是通过 MyClass 引用。
要是你不喜欢虚函数的掺杂,你只需要在定义 MyClass 上定义做一个小的变化:
```
public abstract class MyClass : IMsg
{
public abstract void Message();
}
```
是的,你可以实现接口却没有实际实现这个接口的方法。通过声明接口方法的 abstract 版本,你就是声明继承的子类都必须实现这个接口。 IMsg 是 MyClass 声明的一部分,但是定义的方法被延迟到子类中实现。
子类可以防止进一步的重载的密封方法:
```
public class MyDerivedClass2 : MyClass
{
public sealed override void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
```
另一个解决方案是实现这个的接口中调用一个虚方法,以让子类加入接口的合约中。你可以在 MyClass 中这样做:
```
public class MyClass2 : IMsg
{
protected virtual void OnMessage()
{
}
public void Message()
{
OnMessage();
Console.WriteLine("MyClass");
}
}
```
任何子类重载 OnMessage() 添加它们自己的工作到声明在 MyClass2 的 Message() 方法中。这个模式你在前面类实现 IDisposable 中见过 (查看原则17)。
显式接口实现(查看原则31)使你能够实现 一个接口,也可以隐藏你的类的公共接口。它的使用实现接口和重载虚函数变得不那么清晰。你可以使用显示接口实现限制使用者可以有访问更多的接口方法版本。 IComarable 习惯会在原则31详细展示这点。
还有最后一个添加接口和基类一起工作的惊喜。基类可以提供接口中方法的默认实现。然后,子类可以声明实现这个接口并从基类中继承这个接口的实现,正如下面例子一样。
```
public class DefaultMessageGenerator
{
public void Message()
{
Console.WriteLine("This is a default message");
}
}
public class AnotherMessageGenerator :DefaultMessageGenerator, IMsg
{
// No explicit Message() method needed.
}
```
注意到子类可以声明接口是其的一部分合约,即使它没有提供任何 IMsg 方法的实现。只要它由恰当的公有可访问签名的方法,那么满足接口的合约。使用这个方法,你可以不用显示接口实现。
实现接口比创建和重载虚函数有更多选择。你可以创建 sealed 实现,虚实现,或者是类继承接口的抽象约束。你也可以创建 sealed 实现并提供一个虚函数调用来实现接口。你可以准确地决定怎样和什么时候子类修改你的基类实现的接口的默认行为。接口方法不是虚方法而是独立的合约。
小结:
这个原则没有大量枚举接口和继承各种组合使用的不同,那都是专牛角尖的人才去干的,而是用原理上梳理了下两者的不同,当然也有点坑需要记住的:接口的显示实现会隐藏子类的实现,添加 new 关键字可以解决这个问题,但是还不是多态。
作为接口使用,如果基类没有 virtual 和 子类也没有 new 那么基类实现优先级会更高!!!
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/2083428](/blog/2083428)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable<T> 和 IComparer<T> 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数