1. 前言
如果你現在聽到 覆寫 這個詞有點冒汗,別怕啦~其實一點都不可怕,還超方便的!方法覆寫 就是讓你可以在衍生類別裡,把基底類別的方法行為換成你自己的。這樣我們的程式碼就變得很彈性、很好擴充,也更貼近現實生活,畢竟每隻動物都不想只是「某種聲音」而已。
要讓方法可以被覆寫,基底類別要用 virtual 標記它。衍生類別如果要換掉實作,就要用 override 關鍵字。
範例
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("某種通用的動物聲音...");
}
}
然後在狗狗的類別裡:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
- 基底類別用 virtual。
- 衍生類別用 override。
- 方法簽章(名字、回傳型別、參數)要一模一樣。
視覺圖解
2. 實際運作示範
來看看覆寫實際上怎麼跑。先創幾隻動物來測試聲音:
Animal pet1 = new Animal { Name = "無名動物" };
Dog pet2 = new Dog { Name = "巴布斯" };
pet1.MakeSound(); // 會印出:某種通用的動物聲音...
pet2.MakeSound(); // 會印出:汪汪!
現在來點進階的:
如果我們把狗存進 Animal 型別的變數會怎樣?
Animal pet3 = new Dog { Name = "沙力克" };
pet3.MakeSound(); // ???
你覺得會發生什麼事?
答案: 會印出「汪汪!」
因為雖然變數型別是 Animal,但它「指向」的是狗,所以會呼叫覆寫過的方法!
這就是動態(或延遲)繫結的魔法啦。
3. 覆寫時用 base 關鍵字
有時候你不想整個換掉方法,只想「加料」——比如多做點自己的事,再跑舊的行為。這時就可以用 base 關鍵字,讓你呼叫基底類別的方法版本。
public class Cat : Animal
{
public override void MakeSound()
{
base.MakeSound(); // 呼叫基底實作
Console.WriteLine("喵!");
}
}
執行這個方法時,會先印出「某種通用的動物聲音...」,然後再來「喵!」
4. 覆寫時方法選擇怎麼決定
為了讓你更直觀理解「底層」發生什麼事,想像有這樣一張表(虛擬調度):
| 變數型別 | 物件型別 | 會呼叫哪個方法 |
|---|---|---|
| Animal | Animal | Animal.MakeSound |
| Animal | Dog | Dog.MakeSound |
| Animal | Labrador | Labrador.MakeSound |
| Dog | Labrador | Labrador.MakeSound |
| Dog | Dog | Dog.MakeSound |
最重要的規則:
變數型別只對編譯器有用,執行時會看實際物件型別(就是你用 new 建出來的那個)。
這個機制叫做 動態(或延遲)繫結——多型就是靠這個實現的(下堂課會再講!)。
為什麼要覆寫方法
- 在 GUI 框架:你有個基底視窗類別,要覆寫方法來畫出不同的元件。
- 在遊戲引擎:基底類別 Enemy,繼承類別實作不同行為。
- 在單元測試:可以做「假物件」(stubs, mocks)來取代方法。
現代 .NET 框架超常用這個機制來做事件、樣板程式碼、繼承設定,甚至物件序列化(像是用虛擬屬性)。
5. 用 new 關鍵字隱藏方法
我們剛剛說過,要覆寫方法要用 virtual/override 這對組合。但 C# 還有另一個跟繼承方法有關的修飾詞——就是 new。
new 到底幹嘛用?
new 是給你在衍生類別裡宣告同簽章的方法時用的,但你不想覆寫虛擬方法,而是要隱藏(遮蔽)基底方法。
- 這不是覆寫,是隱藏。
- 這種方法是看變數型別來決定呼叫哪個(沒有動態多型!)。
- 如果你「不小心」沒寫 new 就遮蔽了方法,編譯器會警告你。
範例:override 跟 new 的差別
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("動物發出某種聲音...");
}
}
public class Dog : Animal
{
// 隱藏基底類別的方法(不是覆寫)
public new void MakeSound()
{
Console.WriteLine("這不是 override!只是狗狗的方法。");
}
}
來看一下行為:
Animal a = new Dog();
Dog d = new Dog();
a.MakeSound(); // "動物發出某種聲音..."
d.MakeSound(); // "這不是 override!只是狗狗的方法。"
- 如果變數型別是 Dog——會呼叫 Dog 的方法。
- 如果變數型別是 Animal,即使裡面放的是 Dog——還是呼叫 Animal 的方法!
6. 回饋與實作細節
剛開始寫程式時,常常會遇到「明明覆寫了,怎麼還是舊的行為」這種困惑。通常原因很簡單:基底類別沒寫 virtual,或是衍生類別用 new 而不是 override。第二種特別陰險——如果你用基底型別呼叫方法,會跑基底版本,不會跑覆寫的。所以一定要記得用對關鍵字。
除了語法錯誤,有時新手會想在覆寫時換掉回傳型別。比如基底方法回傳 object,衍生類別想回傳 string。這樣不行啦:方法簽章要完全一樣。
比較表:override vs new
| 特性 | override | new |
|---|---|---|
| 機制 | 覆寫虛擬方法 | 隱藏基底類別的方法 |
| 延遲繫結 | 有——靠動態多型 | 沒有——看變數型別 |
| 基底類別需要... | virtual、abstract 或已經 override | 不用 |
| 建議使用時機 | 幾乎都該用 | 只有特殊情況才用 |
7. 覆寫方法遇到繼承鏈
覆寫這套東西,遇到長繼承鏈時就更有趣了:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("動物做了什麼...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
public class Labrador : Dog
{
public override void MakeSound()
{
Console.WriteLine("我是拉布拉多:汪~汪~!");
}
}
如果我們寫:
Animal pet = new Labrador();
pet.MakeSound(); // => "我是拉布拉多:汪~汪~!"
C# 會永遠選最「深」的虛擬方法實作,也就是物件本身的那個。
8. 覆寫方法常見錯誤
世界不完美,學生(還有老手!)都會犯錯。我們直接來看最常見的「地雷」怎麼避開:
1. 忘了在基底類別寫 virtual
public class Animal
{
public void MakeSound() { ... } // 沒有 'virtual'
}
public class Dog : Animal
{
// 編譯錯誤!不能覆寫。
public override void MakeSound()
{
Console.WriteLine("汪!");
}
}
C# 會直接說:'Dog.MakeSound()': 找不到可覆寫的方法
2. 簽章不一樣
要確定方法名稱、回傳型別、參數都一樣:
public class Animal
{
public virtual void MakeSound() { ... }
}
public class Dog : Animal
{
// 錯誤:簽章不同(比如多加了參數)
public override void MakeSound(string sound)
{
Console.WriteLine(sound);
}
}
3. 沒必要別用 new 取代 override
new 這個關鍵字是用來隱藏基底類別的方法,不是覆寫,也不會有動態多型。這是另一種機制,沒特別理由最好別用。
GO TO FULL VERSION