首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 多型核心:virtual、override、new 關鍵字深度解析與實戰陷阱

     編輯:關於C語言

前言

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    ❌ 非多型

IL 層面觀察 (關鍵指令)

// virtual + override
.callvirt instance string BaseClass::VirtualMethod()  // 動態派發

// new 隱藏
.call instance string DerivedClass::NonVirtualMethod()  // 靜態呼叫 (derived 參考)
.call instance string BaseClass::NonVirtualMethod()     // 靜態呼叫 (baseRef 參考)

callvirt 會在執行期根據物件實際型別查閱方法表;call 則在編譯期根據參考型別直接綁定。

常見陷阱與最佳實踐

陷阱 1:忘記 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 消除警告並表達意圖。

陷阱 2: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 保持多型契約。

陷阱 3:屬性隱藏與介面實作衝突

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";) 避免隱藏語意模糊。

最佳實踐清單

  1. 設計給繼承用的方法預設加 virtual,並附上 XML 文件說明覆寫契約。
  2. 覆寫時務必加 override,編譯器會驗證基類確有 virtual/abstract。
  3. 避免使用 new 隱藏 virtual 成員,除非為了相容舊版組件且無法修改基類。
  4. 封裝不變行為用 sealed:public sealed override void CriticalMethod() 防止進一步覆寫。
  5. 單元測試驗證多型行為:針對基類參考呼叫虛擬方法,斷言執行衍生類邏輯。

進階:預設介面方法 (C# 8.0+)

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 的類別階層。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved