# 原則34:避免定義在基類的方法的重寫
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
當基類給成員命名時,名字就賦予了語義。在任何情況下,子類最好都不要將同一個名字用作其他目的。但為什么有很多其他原因子類會使用同一個名字。它可能想用不同方式實現同一個語義,或者有不同的參數。有時這是語言原生就支持:類設計者可以聲明一個虛函數,子類就可以各自實現語義。原則33包含了為什么使用 new 修飾符 可能導致很難發現代碼的 bug 。在這個原則里,你將會學到重寫基類定義的函數會導致類似的問題。你應該不要重寫基類聲明的方法。
C# 語言重載解析的規則必然是非常復雜的。所有可能的子類聲明的方法,基類的任何方法,以及擴展方法和實現接口的方法都是解析的候選方法。增加了泛型方法和泛型擴展方法,就變得更復雜。使用默認參數,而且我不確定每個人能否確切知道結果是什么。你真的想要這個情況更加復雜?創建的聲明在基類函數的重寫版本會增加找到最好函數匹配的復雜性。這也增加歧義的可能性。這也增加你的解釋和編譯器不同的可能性,就自然困擾使用者。解決方法是很簡單的:給方法選擇不同的名字。這是你的類,你就有足夠的光彩去給方法提出一個不同名字,尤其當很容易困擾類的使用者時。
這個指導是直截了當的,然而總是有人會懷疑真的要這么嚴格。可能是因為重寫聽起來很像重載。重載虛函數是面向對象語言的核心原則;那明顯不是我的意思。重寫是使用不同的參數列表創建多個名字一樣的方法。重寫基類方法真的對重寫解析有很多影響?我們從不同的方式看在重寫基類中的方法會引起問題。
這個問題有很多組合情況。我們從最簡單的開始。很多時候基類和子類更多的是不同參數列表的重寫。字啊么所有的例子,任何基類類名以“B”開始,任何子類類名以“D”開始。下這個例子用這個繼承關系的類作為參數:
```
public class B2 {}
public class D2 : B2 {}
```
下面這個類的方法使用子類(D2)作為參數:
```
public class B
{
public void Foo(D2 parm)
{
Console.WriteLine("In B.Foo");
}
}
```
顯然,這段代碼會輸出“In B.Foo”:
```
var obj1 = new D();
obj1.Bar(new D2());
```
下面,我們在子類中重寫這個方法:
```
public class D : B
{
public void Foo(B2 parm)
{
Console.WriteLine("In D.Foo");
}
}
```
那么,執行下面這段代碼會發生什么?
```
var obj2 = new D();
obj2.Foo(new D2());
obj2.Foo(new B2());
```
兩行都是輸出“In D.Foo”。你調用的都是子類的方法。很多開開發者會認為第一個函數調用會輸出“In B.Foo”。然而,即使很簡單的重寫都會很驚奇。兩個調用都解析為 D.Foo 的原因是當有多個候選方法時,編譯時繼承關系最底端的子類的方法是最好的選擇。即使當基類有更好的匹配這個規則都是正確的。當然,也是非常脆弱的。你認為下面的結果是什么:
```
B obj3 = new D();
obj3.Foo(new D2());
```
上面我的用詞非常小心因為 obj3 的編譯時類型為 B (你的基類),即使它的運行時類型是 D (你的子類)。 Foo 不是虛函數, obj3.Foo() 一定會被解析為 B.Foo 。
如果你的使用者基礎差并想要解析規則跟他們期望的一樣,他們就需要使用強制類型轉換:
```
var obj4 = new D();
((B)obj4).Foo(new D2());
obj4.Foo(new B2());
```
如果你的 API 強制你的使用者這樣構造,你就會遇到很多挫折。你還可以很容易增加一點困擾。在你的基類 B 增加一個方法:
```
{
public void Foo(D2 parm)
{
Console.WriteLine("In B.Foo");
}
public void Bar(B2 parm)
{
Console.WriteLine("In B.Bar");
}
}
```
毫無疑問,下面的代碼會打印出“In B.Bar”:
```
var obj1 = new D();
obj1.Bar(new D2());
```
現在,增加另一種重寫,包含一個默認參數:
```
public class D : B
{
public void Foo(B2 parm)
{
Console.WriteLine("In D.Foo");
}
public void Bar(B2 parm1, B2 parm2 = null)
{
Console.WriteLine("In D.Bar");
}
}
```
希望,你已經看到將會發生什么。同樣的代碼現在會打印出“In D.Bar”(你又調用子類):
```
var obj1 = new D();
obj1.Bar(new D2());
```
唯一的調用基類的方法的方式是在調用代碼中提供強制類型轉換。
這幾個例子展示一個參數的方法會遇到的問題。如果你的參數是基于泛型的會變得越來越復雜。假設你增加下面的方法:
```
public class B
{
public void Foo(D2 parm)
{
Console.WriteLine("In B.Foo");
}
public void Bar(B2 parm)
{
Console.WriteLine("In B.Bar");
}
public void Foo2(IEnumerable<D2> parm)
{
Console.WriteLine("In B.Foo2");
}
}
```
進而,在子類添加不同的重寫:
```
public class D : B
{
public void Foo(B2 parm)
{
Console.WriteLine("In D.Foo");
}
public void Bar(B2 parm1, B2 parm2 = null)
{
Console.WriteLine("In D.Bar");
}
public void Foo2(IEnumerable<B2> parm)
{
Console.WriteLine("In D.Foo2");
}
}
```
按照前面的方式調用 Foo2 :
```
var sequence = new List<D2> { new D2(), new D2() };
var obj2 = new D();
obj2.Foo2(sequence);
```
這回你會認為輸出什么?如果你花了心思,你會發現“ In D.Foo2 ”會被輸出。你會這個答案半信半疑。這個就是 C# 4.0 的變化。從 C# 4.0 開始,泛型接口支持協變和逆變,這意味 D.Foo2 是參數為 IEnumerable<D2> 的候選方法,盡管它的參數類型是 IEnumerable<B2> 。然后,更早的 C# 版本泛型不具有可變性。也就是說泛型參數是不可變的。在那些版本, 當參數為 IEnumerable<D2> 時,D.Foo2 就不是候選方法。唯一的的候選方法是 B.Foo2 ,在那些版本中它是正確的答案。
上面的代碼表明,有時很多復雜的情況你需要強制類型轉換幫助編譯器選擇你想要的方法。在現實世界中,毫無疑問你會遇到需要使用強制類型轉換而不是靠編譯器選擇“最好”的方法的情況,因為類繼承關系,實現接口和擴展方法一起組成你想要的方法。但事實上,現實世界丑陋的情況偶爾才發生不意味著你創建更多的重寫方法給自己增加更多問題。
現在你就可以在程序員的雞尾酒會因擁有更深入 C# 重載解析而讓你朋友震驚。這很有用的信息,并且你對語言了解的更多,你就會是更好的開發者。但是不要期望你的使用者會有和你一樣層次的知識。更重要的是,不要以為使用你的 API 的每個人都對重寫解析怎么工作的都有詳細的掌握。而是,不要重寫在基類聲明的方法。那樣不會提供任何作用,并且只能是導致你的使用者的困擾。
小結:
記住,重寫解析規則是優先選擇編譯時繼承結構最底端的子類的方法,即使基類有更匹配的方法,C# 4.0以后版本泛型也遵循這個規則,避免重寫,這則原則的要義就掌握了!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2088635](/blog/2088635)
更多精彩請關注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&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數