C# 多型核心在於 virtual、override、new 三個關鍵字的互動。許多開發者混淆「覆寫」與「隱藏」,導致執行期行為不符預期。本文透過 IL 層面解析、多型呼叫實測、版本控制語意、最佳實務,徹底釐清兩者差異。
| 關鍵字 | 位置 | 語意 | IL 指令 |
|---|---|---|---|
virtual | 基底類別方法 | 宣告此方法**可被衍生類別覆寫**,支援多型調度 | callvirt |
override | 衍生類別方法 | **覆寫**基底虛擬/抽象方法,**保持多型** (虛擬表替換槽位) | callvirt (槽位相同) |
new | 衍生類別方法 | **隱藏**基底同名方法,**中斷多型** (新增虛擬表槽位,非虛擬則無槽位) | call (非虛擬) 或 callvirt (新槽位) |
| (無修飾) | 衍生類別方法 | 同 new,**編譯器警告 CS0108** (建議加 new 明確語意) | 同 new |
public abstract class Animal
{
public virtual string Speak() => "Animal sound"; // 虛擬方法
}
public class Dog : Animal
{
public override string Speak() => "Woof!"; // 覆寫:槽位替換
}
public class Cat : Animal
{
public new string Speak() => "Meow!"; // 隱藏:新槽位 (非虛擬)
}
// --- 測試 ---
Animal a1 = new Dog();
Animal a2 = new Cat();
Dog d = new Dog();
Cat c = new Cat();
Console.WriteLine(a1.Speak()); // "Woof!" 多型:呼叫 Dog.Speak
Console.WriteLine(a2.Speak()); // "Animal sound" 靜態繫結:Animal.Speak (Cat 隱藏未進虛擬表)
Console.WriteLine(d.Speak()); // "Woof!"
Console.WriteLine(c.Speak()); // "Meow!"
Animal 虛擬表 (vtable): Slot 0: Speak() -> Animal.Speak Dog 虛擬表: Slot 0: Speak() -> Dog.Speak (override 覆寫同槽位) Cat 虛擬表: Slot 0: Speak() -> Animal.Speak (Cat.Speak 非虛擬,不佔槽位) Slot 1: (Cat 新增虛擬方法才會佔槽位)
C# 設計目標:**版本安全**。假設基底類別庫 v1 有方法 Calculate()。衍生類別覆寫它。基底類別庫 v2 新增 virtual Calculate()。
override,視為 **new 隱藏** → **編譯警告 CS0108**,強迫開發者明確選擇:
override:確認要覆寫新虛擬方法。new:確認要隱藏,保持原行為。| 情境 | 規則 | 範例 |
|---|---|---|
abstract 方法 | **必須** override,不可 new 隱藏 (編譯錯誤) | public abstract void Do(); → public override void Do() |
| 介面方法 | 隱含 virtual,實作類別用 override (C# 8+ 可預設實作) | interface I { void M(); } → class C : I { public void M() {} } (隱含 override) |
sealed override | **封鎖**進一步覆寫,子類別只能 new 隱藏 | public sealed override void Save() |
| 靜態方法 | **不可** virtual/override/new (隱藏靜態僅編譯期繫結) | static void M() |
| 建構子 | 不可虛擬,無多型 |
class Base { public virtual void Init() { Console.WriteLine("Base"); } }
class Derived : Base { public void Init() { Console.WriteLine("Derived"); } } // 警告 CS0108
Base b = new Derived();
b.Init(); // 輸出 "Base" !!! 非預期
修正: 加 override 或刻意加 new 並理解行為差異。
Base b = new Derived();
b.NewMethod(); // 編譯錯誤:Base 無此方法
// 必須轉型
((Derived)b).NewMethod(); // 才能呼叫
typeof(Derived).GetMethod("M").GetBaseDefinition()
var m = typeof(Derived).GetMethod("Speak");
var baseDef = m.GetBaseDefinition();
Console.WriteLine(baseDef.DeclaringType == typeof(Derived) ? "新增/隱藏" : "覆寫");
virtual,文件說明覆寫契約。override,IDE 自動完成會帶上。new,消除警告、表達意圖。sealed override 防止子類別意外改變核心邏輯。new 隱藏語意 (雖 C# 編譯器警告保護,但仍是破壞性變更)。Base b = new Derived(); b.Method() 確保呼叫正確實作。override = **擴充多型** (虛擬表槽位替換);new = **中斷多型** (靜態繫結/新槽位)。C# 強制明確宣告,是為了**版本演進安全**。理解虛擬表機制、IL 差異、版本語意,即能自信駕馭繼承體系,避免「明明覆寫了卻呼叫基底方法」的經典坑。