C# 多型是物件導向設計的基石。virtual、override、new 三個關鍵字雖然語法簡單,但組合起來卻藏著許多「靜態繫結 vs 動態繫結」的微妙行為差異。本文透過完整程式碼示範、IL 觀點解析、常見陷阱與最佳實踐,幫助開發者徹底掌握多型機制。
| 關鍵字 | 用途 | 繫結時機 | 多型行為 |
|---|---|---|---|
virtual | 基類宣告可被覆寫的方法 | 執行期 (動態) | ✅ 支援 |
override | 衍生類實作覆寫,必須配合 virtual/abstract | 執行期 (動態) | ✅ 支援 |
new | 衍生類隱藏基類同名成員 (非多型) | 編譯期 (靜態) | ❌ 不支援 |
| (無關鍵字) | 衍生類預設隱藏基類,編譯器發出 CS0108 警告 | 編譯期 (靜態) | ❌ 不支援 |
using System;
namespace PolymorphismDemo
{
public class BaseClass
{
public virtual string VirtualMethod() => "Base:Virtual";
public virtual string VirtualProperty { get; } = "Base:VirtualProp";
public string NonVirtualMethod() => "Base:NonVirtual";
public string NonVirtualProperty { get; } = "Base:NonVirtualProp";
}
public class DerivedClass : BaseClass
{
// 1. 正確覆寫 virtual 方法
public override string VirtualMethod() => "Derived:Virtual";
// 2. 覆寫 virtual 屬性
public override string VirtualProperty { get; } = "Derived:VirtualProp";
// 3. 使用 new 明確隱藏非 virtual 成員
public new string NonVirtualMethod() => "Derived:NonVirtual (new)";
// 4. 未加關鍵字隱藏 (編譯器警告 CS0108)
public string NonVirtualProperty { get; } = "Derived:NonVirtualProp (implicit new)";
// 5. 新增衍生類專有方法
public string DerivedOnly() => "Derived:Only";
}
public class Program
{
public static void Main()
{
var derived = new DerivedClass();
BaseClass baseRef = derived; // 向上轉型
Console.WriteLine("=== 透過衍生類別參考 (derived) ===");
Console.WriteLine($"VirtualMethod: {derived.VirtualMethod()}");
Console.WriteLine($"VirtualProperty: {derived.VirtualProperty}");
Console.WriteLine($"NonVirtualMethod: {derived.NonVirtualMethod()}");
Console.WriteLine($"NonVirtualProperty:{derived.NonVirtualProperty}");
Console.WriteLine($"DerivedOnly: {derived.DerivedOnly()}");
Console.WriteLine("\n=== 透過基類別參考 (baseRef) ===");
Console.WriteLine($"VirtualMethod: {baseRef.VirtualMethod()}");
Console.WriteLine($"VirtualProperty: {baseRef.VirtualProperty}");
Console.WriteLine($"NonVirtualMethod: {baseRef.NonVirtualMethod()}");
Console.WriteLine($"NonVirtualProperty:{baseRef.NonVirtualProperty}");
// baseRef.DerivedOnly() // 編譯錯誤:基類別無此成員
}
}
}
=== 透過衍生類別參考 (derived) ===
VirtualMethod: Derived:Virtual
VirtualProperty: Derived:VirtualProp
NonVirtualMethod: Derived:NonVirtual (new)
NonVirtualProperty:Derived:NonVirtualProp (implicit new)
DerivedOnly: Derived:Only
=== 透過基類別參考 (baseRef) ===
VirtualMethod: Derived:Virtual ← 動態繫結:呼叫衍生類實作
VirtualProperty: Derived:VirtualProp ← 動態繫結
NonVirtualMethod: Base:NonVirtual ← 靜態繫結:呼叫基類實作!
NonVirtualProperty:Base:NonVirtualProp ← 靜態繫結
derived.VirtualMethod() ──► 查虛擬方法表 (v-table) ──► Derived.VirtualMethod ✅ 多型
baseRef.VirtualMethod() ──► 查虛擬方法表 (v-table) ──► Derived.VirtualMethod ✅ 多型
derived.NonVirtualMethod() ──► 編譯期解析 Derived.NonVirtualMethod ✅ 隱藏
baseRef.NonVirtualMethod() ──► 編譯期解析 Base.NonVirtualMethod ❌ 非多型
// virtual + override
.callvirt instance string BaseClass::VirtualMethod() // 動態派發
// new 隱藏
.call instance string DerivedClass::NonVirtualMethod() // 靜態呼叫 (derived 參考)
.call instance string BaseClass::NonVirtualMethod() // 靜態呼叫 (baseRef 參考)
callvirt 會在執行期根據物件實際型別查閱方法表;call 則在編譯期根據參考型別直接綁定。
virtual 導致非預期隱藏public class Base { public void Do() {} }
public class Derived : Base { public new void Do() {} } // 編譯警告 CS0108
Base b = new Derived();
b.Do(); // 呼叫 Base.Do(),非 Derived.Do()!
對策:基類預設加 virtual,或刻意設計為 sealed 禁止覆寫。若確需隱藏,明確加 new 消除警告並表達意圖。
new 破壞 Liskov 替換原則 (LSP)public class Animal { public virtual void Speak() => Console.WriteLine("..."); }
public class Dog : Animal { public new void Speak() => Console.WriteLine("汪"); }
Animal a = new Dog();
a.Speak(); // 輸出 "..." 而非 "汪",違反 LSP
對策:除非極少數相容性場景,嚴禁對 virtual 成員使用 new。改用 override 保持多型契約。
public interface IConfig { string Value { get; } }
public class BaseConfig : IConfig { public virtual string Value => "Base"; }
public class DerivedConfig : BaseConfig, IConfig
{
public new string Value => "Derived"; // 明確介面實作會更清晰
}
建議使用 明確介面實作 (string IConfig.Value => "Derived";) 避免隱藏語意模糊。
virtual,並附上 XML 文件說明覆寫契約。override,編譯器會驗證基類確有 virtual/abstract。new 隱藏 virtual 成員,除非為了相容舊版組件且無法修改基類。sealed:public sealed override void CriticalMethod() 防止進一步覆寫。public interface ILogger
{
void Log(string msg) => Console.WriteLine($"[DEFAULT] {msg}"); // 預設實作
}
public class ConsoleLogger : ILogger { } // 繼承預設
public class FileLogger : ILogger
{
public void Log(string msg) => File.AppendAllText("log.txt", msg + "\n"); // 覆寫
}
介面預設方法提供「多重繼承行為」替代方案,但同樣遵循 callvirt 動態派發語意。
virtual/override 是 C# 多型的核心,提供執行期動態派發;new 是靜態隱藏機制,破壞多型契約。掌握 「參考型別決定靜態繫結,實際型別決定動態繫結」 這一原則,配合編譯器警告 (CS0108)、sealed、明確介面實作等工具,即能設計出清晰、可維護、符合 LSP 的類別階層。