本文透過四個版本的範例程式,層層剖析 C# 中 new、virtual、override 的行為差異,並解釋早期繫結與晚期繫結的底層機制。
public class Father { public void Run0() { Console.WriteLine("Father.Run0"); } }
public class Son : Father { public void Run0() { Console.WriteLine("Son.Run0"); } }
Father[] list = new Father[2];
list[0] = new Father();
list[1] = new Son();
list[0].Run0(); // Father.Run0
list[1].Run0(); // Father.Run0 ← 注意!非 Son.Run0
結果:皆輸出 Father.Run0。
編譯警告:Son.Run0() 隱藏繼承成員 Father.Run0(),建議加 new 關鍵字。
機制:早期繫結 — 編譯期依參考型別 (Father) 決定呼叫基類方法。
new (顯式隱藏)public class Son : Father { public new void Run1() { Console.WriteLine("Son.Run1"); } }
結果:同版本一,皆輸出 Father.Run1。
結論:new 僅消除警告,明確表達「隱藏基類方法」意圖,不改變呼叫行為。C# 隱式為同名同參方法加 new。
virtual,子類無修飾詞public class Father { public virtual void Run2() { Console.WriteLine("Father.Run2"); } }
public class Son : Father { public void Run2() { Console.WriteLine("Son.Run2"); } }
結果:仍輸出兩次 Father.Run2。
警告:Son.Run2() 將隱藏繼承成員,若要覆寫請加 override,否則加 new。
關鍵:virtual 僅表示「允許被覆寫」,若子類未加 override,仍視為隱藏,早期繫結生效。
virtual + 子類 override (真正多型)public class Son : Father { public override void Run3() { Console.WriteLine("Son.Run3"); } }
結果:
Father.Run3
Son.Run3 ← 參考型別 Father,實際型別 Son,呼叫 Son 方法!
機制:晚期繫結 — 執行期依實際物件型別 決定呼叫方法。
| 關鍵字/組合 | 繫結時機 | 行為 | 適用場景 |
|---|---|---|---|
無修飾詞 / new | 編譯期 (早期繫結) | 隱藏基類方法,參考型別決定呼叫 | 非多型設計、刻意切斷繼承鏈 |
virtual (基類) + 無修飾詞 (子類) | 編譯期 (早期繫結) | 子類隱藏基類虛擬方法,非多型 | 幾乎無合理用途,通常為錯誤 |
virtual + override | 執行期 (晚期繫結) | 真正多型,實際型別決定呼叫 | 標準多型設計、擴充點 |
virtual/override 明確宣告多型意圖,避免意外覆寫對比 Java:所有方法預設為虛擬,執行期自動檢測實際型別,帶來微小效能成本但語法更簡潔。
virtual,除非確定需提供擴充點virtual,子類 必須 overridenew 消除警告並文件化意圖virtual + 子類無修飾詞 的組合 (極易誤導)sealed override 防止進一步覆寫new = 隱藏 (早期繫結);virtual = 允許覆寫;override = 實作覆寫 (晚期繫結)。三者缺一不可構成真正多型。理解早/晚期繫結差異,是掌握 C# 物件導向精髓的關鍵。