# 原則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:使用動態接收匿名類型參數