1. 多型實戰
在寫程式時,多型就像萬用遙控器:你按一下 "音量+",結果可能是電視、音響或冷氣在動作——每台機器反應都不一樣,但介面只有一個!同理,不同型別的物件可以對同一個方法呼叫有不同反應,只要這個方法在它們的共同祖先裡有被定義成 virtual。
在 C# 裡,多型的威力就是:基底類別(或介面)型別的變數可以「裝」任何它的子類物件,然後呼叫 virtual 方法時,會執行真正物件自己定義的版本。這就是很多架構可以動態換邏輯的基礎。
這除了教科書以外有用嗎?
當然有!幾乎所有專案,只要你有處理不同但類似的物件——從動物園的動物到 UI 元件、事件處理器、文件系統,通通都用得到。
- 可以寫出通用演算法——你的程式只管抽象的東西,不用管細節。
- 保證可擴充——你要加一百種新「動物」、「圖形」、「處理器」都不用動舊程式碼。
- 減少元件之間的耦合(這是面試和架構設計很愛問的重點!)。
2. 基本語法和機制
來回顧一下我們的 Animal、Dog、Cat 類別,順便加強一下多型的概念。既然我們從「虛擬動物園」開始,就繼續進化它吧。
有 virtual 方法的基底類別
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// 虛擬方法——可以被覆寫
public virtual void MakeSound()
{
Console.WriteLine($"{Name} 發出某種聲音...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:汪汪!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:喵!");
}
}
多型應用:動物集合的例子
假設我們有一個動物清單——有家裡的也有野生的——我們想讓它們全部「叫」一聲。沒有多型你就得寫一堆型別判斷,重複的程式碼一大堆。有多型就超優雅又精簡!
// 建立不同種類的動物陣列
Animal[] animals = new Animal[]
{
new Dog("Bobik"),
new Cat("Murka"),
new Dog("Sharik"),
new Cat("Barsik"),
};
// 走訪整個陣列,讓每隻動物叫一聲
foreach (var animal in animals)
{
animal.MakeSound(); // 會呼叫 Dog 或 Cat 的方法,不是 Animal 的!
}
結果:
Bobik 說:汪汪!
Murka 說:喵!
Sharik 說:汪汪!
Barsik 說:喵!
這就是魔法:一份通用程式碼,根據物件的真實型別產生不同結果。
圖解:多型怎麼運作
Animal(基底類別)
/ \
Dog Cat
當你呼叫 animal.MakeSound(),而 Animal 可能裝的是 Dog 或 Cat,.NET 執行時會自動判斷該叫哪個 MakeSound()。
3. 用多型解決常見問題
例子 1:通用清單,不同動作
假設你在做遊戲。有個基底類別 GameObject,衍生類別有敵人、盟友、障礙物。大家都會移動,都有 Update() 方法,但實作內容不一樣。
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("敵人進攻!");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("盟友幫忙!");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// 會輸出:
// 敵人進攻!
// 盟友幫忙!
// 敵人進攻!
例子 2:把物件傳進方法
你可以接受基底型別的參數,實際上卻可以用任何子類。這超省力,尤其是以後還會有新子類。
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
Console.WriteLine("而且拿到食物。");
}
FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Sima"));
// 結果:
// Rex: Rex 說:汪汪!
// 而且拿到食物。
// Sima: Sima 說:喵!
// 而且拿到食物。
注意:沒有多型你要寫這種方法會超麻煩——要判斷動物型別、寫一堆 if else,還要手動呼叫正確的方法。
重點:執行時期綁定
多型靠的是 virtual 方法呼叫動態發生,也就是執行時期才決定。這叫做晚期綁定(late binding)。就算你變數型別是 Animal,方法還是會呼叫真實物件裡的版本。
如果方法沒標 virtual,呼叫時就只會執行變數型別那個版本(不是物件的)。所以想要彈性,virtual 千萬別省!
4. 實作練習:擴充我們的應用程式
假設你想在虛擬動物園加新功能:每隻動物除了 MakeSound(),還要能移動(Move())。但每種動物移動方式都不一樣。
1. 在基底類別加 virtual 方法
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} 發出某種聲音...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} 以不明方式移動...");
}
}
2. 子類各自實作 Move()
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:汪汪!");
}
public override void Move()
{
Console.WriteLine($"{Name} 追著樹枝跑。");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:喵!");
}
public override void Move()
{
Console.WriteLine($"{Name} 輕輕地走著。");
}
}
3. 在集合裡用兩個方法
Animal[] animals = new Animal[]
{
new Dog("Bim"),
new Cat("Lusia")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
結果:
Bim 說:汪汪!
Bim 追著樹枝跑。
Lusia 說:喵!
Lusia 輕輕地走著。
在實際應用裡,這種做法讓你寫出很強大又可重用的模組。像遊戲裡的物件管理器——從 NPC 到特效,都是這樣搞的。
5. 實戰題:畫不同圖形
來看個不是動物園的例子,這次是圖形。先寫個基底類別 Shape,有個 virtual 方法 Draw(),然後再擴充它。
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("畫出不明圖形。");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("畫出圓形。");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("畫出長方形。");
}
}
// 圖形集合
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
這裡 Draw() 是在 Shape 型別的變數上呼叫,但實際上會跑到 Circle 或 Rectangle 的方法。在真的圖形編輯器或像 WinForms、WPF 這種函式庫裡,就是這樣運作的。
6. 通用寫法:打造萬用演算法
多型讓你的程式碼變超彈性又好擴充。舉例來說,集合裡不只可以有 Dog 和 Cat,還可以有 Hamster、Parrot、隨便什麼——你不用多寫一行,就能用同一套方法處理它們。
而且你也可以很輕鬆把子類物件傳進只接受基底型別的方法:
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"名字:{animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Homa");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // 用 Animal 的方法
PrintAnimalInfo(dog); // 用 Dog 的版本
7. 有用的小細節
常見問題和陷阱
很多新手會以為,建立 Dog 物件後,宣告變數 Dog myDog 跟 Animal myDog 沒差。其實,如果你用 Animal 宣告,變數只「看得到」 Animal 裡的東西(除了被覆寫的方法),用 Dog 宣告才會看到 Bark() 和專屬屬性。
還有一點很重要:如果基底類別的方法沒標 virtual,你不能覆寫它。如果你硬要在子類寫 override,編譯器會直接報錯。
對了,如果你真的想「蓋掉」一個不是 virtual 的方法,可以用 new,但這是進階(而且容易出事!)的用法。
為什麼面試很愛問這個?
因為多型就像程式設計師的瑞士刀:你懂怎麼用,就能寫出可擴充又好維護的系統,程式不只會動,還能活很久。像是面試會叫你做不同付款方式(經典:BankCard、PayPal、Bitcoin)——他們希望你寫一個共用介面(或抽象基底類別)有個 Pay() 方法,然後客戶端可以呼叫 Pay(BankCard)、Pay(PayPal)、Pay(Bitcoin),完全不用管裡面怎麼實作。
8. 新手常犯的錯
最常見的錯誤之一,就是想用基底型別的變數呼叫子類才有的方法。像這樣:
Animal animal = new Dog("Tuzik");
animal.Bark(); // 錯誤!Animal 沒有 Bark 方法。
為什麼不行?因為 Animal 型別的變數只「看得到」 Animal 裡有的東西,就算實際上是 Dog。你只能呼叫基底類別有定義、而且有被 override 的方法。
如果你真的要呼叫只有 Dog 才有的方法——就要做型別轉換:
Animal animal = new Dog("Tuzik");
if (animal is Dog dog)
{
dog.Bark();
}
不過通常你這樣寫,代表你的架構有點問題(或你太愛繼承了)。
GO TO FULL VERSION