在 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"
virtual/override 建立虛擬方法表,實現動態派發a 時,透過虛擬方法表解析至衍生類實作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 參考呼叫時使用基類成員class ClassA
{
protected string a = null; // 未初始化
public void Display() => Console.WriteLine(a);
}
class ClassB : ClassA
{
protected string a = "another test"; // 隱藏基類欄位
}
// 執行 instance.Display() 拋出 NullReferenceException
ClassB 的 a 會被使用ClassA.Display() 存取的是 ClassA.a(值為 null)Console.WriteLine(null) 不會拋出例外,但若後續對 null 呼叫方法則會觸發 NullReferenceException| 情境 | 建議做法 | 避免做法 |
|---|---|---|
| 需要多型行為 | 基類用 virtual 方法/屬性,衍生類用 override |
使用同名欄位 |
| 衍生類確實需要新成員 | 改用不同名稱(如 _derivedValue) |
直接隱藏基類欄位 |
| 必須隱藏基類實作 | 明確加上 new 關鍵字並加註解說明原因 |
省略 new 導致編譯器警告被忽略 |
| 設計基類 | 欄位設為 private,透過 protected virtual 屬性暴露 |
使用 protected 欄位 |
new 欄位一致virtual 關鍵字,欄位隱藏行為同 C#,但支援多重繼承增加複雜度override、強制 new 隱藏,降低意外多型風險理解「欄位隱藏屬於編譯時期靜態綁定,方法覆寫屬於執行時期動態派發」是避免此類 Bug 的關鍵。永遠優先使用私有欄位搭配虛擬屬性/方法建立可擴充的類別階層,將隱藏機制作為罕見的刻意設計選項而非預設行為。遇到繼承相關的異常行為時,檢查成員是否加上 virtual/override/new 修飾詞,通常能快速定位問題根源。