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

C# 繼承陷阱:欄位隱藏 vs 方法覆寫的行為差異深度解析

     編輯:關於C語言

前言:一個令人困惑的現象

在 C# 等強型別物件導向語言中,繼承是核心機制之一。然而,許多開發者對「欄位隱藏」與「方法覆寫」的行為差異存在誤解,導致難以察覺的 Bug。本文透過三個遞進的程式碼範例,深入剖析基類與衍生類同名成員在不同修飾詞下的運行時行為,並解釋為什麼會產生 NullReferenceException 這類看似不可思議的錯誤。

場景一:欄位隱藏 —— 編譯時期綁定

class ClassA
{
    protected string a = "test";
    public void Display() => Console.WriteLine(a);
}

class ClassB : ClassA
{
    protected string a = "another test"; // 隱藏基類欄位
}

// 測試程式碼
ClassB instance = new ClassB();
instance.Display();           // 輸出: test
((ClassA)instance).Display(); // 輸出: test

核心解析

  • 非虛擬方法:Display() 未標記 virtual,屬於靜態綁定(編譯時期決定呼叫哪個方法)
  • 欄位隱藏:ClassB 宣告新欄位 a,僅遮蔽基類同名欄位,不影響基類方法存取自身欄位
  • 記憶體佈局:實例包含兩個獨立的 a 欄位,ClassA.Display() 永遠存取 ClassA 的 a
  • 強制轉型:(ClassA)instance 不改變物件本身,僅改變編譯器視角,呼叫仍是 ClassA.Display()

場景二:虛擬屬性覆寫 —— 執行時期多型

class ClassA
{
    private string _a = "test";
    protected virtual string a 
    { 
        get => _a; 
        set => _a = value; 
    }
    public void Display() => Console.WriteLine(a);
}

class ClassB : ClassA
{
    private string _deriveda = "another string";
    protected override string a 
    { 
        get => _deriveda; 
        set => _deriveda = value; 
    }
}

// 測試結果:兩次皆輸出 "another string"

關鍵差異

  1. virtual/override 建立虛擬方法表,實現動態派發
  2. 屬性存取器實際上是方法,受多型機制管控
  3. 基類方法呼叫 a 時,透過虛擬方法表解析至衍生類實作
  4. 物件單一性:只有一個邏輯上的 a 屬性,行為由實際物件類型決定

場景三:新增成員與方法隱藏

class ClassA
{
    private string _a = "test";
    protected virtual string a { get => _a; set => _a = value; }
    public virtual void Display() => Console.WriteLine(a);
}

class ClassB : ClassA
{
    private string _deriveda = "another string";
    protected new string a { get => _deriveda; set => _deriveda = value; } // 隱藏而非覆寫
    public new void Display() => Console.WriteLine(a); // 隱藏基類方法
}

// 測試結果:
instance.Display();           // 輸出: another string
((ClassA)instance).Display(); // 輸出: test

行為解析

  • new 關鍵字明確表示「隱藏」而非「覆寫」
  • ClassB.Display() 與 ClassA.Display() 是兩個獨立方法
  • 透過 ClassB 參考呼叫時使用衍生類成員;透過 ClassA 參考呼叫時使用基類成員
  • 這是「靜態綁定」與「動態派發」共存的混合情況

實戰錯誤案例:NullReferenceException 根因

class ClassA
{
    protected string a = null; // 未初始化
    public void Display() => Console.WriteLine(a);
}

class ClassB : ClassA
{
    protected string a = "another test"; // 隱藏基類欄位
}

// 執行 instance.Display() 拋出 NullReferenceException

錯誤鏈條

  1. 開發者預期 ClassB 的 a 會被使用
  2. 實際上 ClassA.Display() 存取的是 ClassA.a(值為 null)
  3. Console.WriteLine(null) 不會拋出例外,但若後續對 null 呼叫方法則會觸發 NullReferenceException
  4. 編譯器僅發出警告 CS0108(「以新關鍵字隱藏繼承成員」),不阻止編譯

最佳實踐指南

情境建議做法避免做法
需要多型行為 基類用 virtual 方法/屬性,衍生類用 override 使用同名欄位
衍生類確實需要新成員 改用不同名稱(如 _derivedValue) 直接隱藏基類欄位
必須隱藏基類實作 明確加上 new 關鍵字並加註解說明原因 省略 new 導致編譯器警告被忽略
設計基類 欄位設為 private,透過 protected virtual 屬性暴露 使用 protected 欄位

跨語言對比補充

  • Java:所有非靜態方法預設為虛擬,欄位永遠隱藏不支援多型,行為與 C# new 欄位一致
  • C++:需顯式 virtual 關鍵字,欄位隱藏行為同 C#,但支援多重繼承增加複雜度
  • C# 設計哲學:預設非虛擬、明確 override、強制 new 隱藏,降低意外多型風險

總結

理解「欄位隱藏屬於編譯時期靜態綁定,方法覆寫屬於執行時期動態派發」是避免此類 Bug 的關鍵。永遠優先使用私有欄位搭配虛擬屬性/方法建立可擴充的類別階層,將隱藏機制作為罕見的刻意設計選項而非預設行為。遇到繼承相關的異常行為時,檢查成員是否加上 virtual/override/new 修飾詞,通常能快速定位問題根源。

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