CodeGym /課程 /C# SELF /實戰多型的應用

實戰多型的應用

C# SELF
等級 21 , 課堂 3
開放

1. 多型實戰

在寫程式時,多型就像萬用遙控器:你按一下 "音量+",結果可能是電視、音響或冷氣在動作——每台機器反應都不一樣,但介面只有一個!同理,不同型別的物件可以對同一個方法呼叫有不同反應,只要這個方法在它們的共同祖先裡有被定義成 virtual

在 C# 裡,多型的威力就是:基底類別(或介面)型別的變數可以「裝」任何它的子類物件,然後呼叫 virtual 方法時,會執行真正物件自己定義的版本。這就是很多架構可以動態換邏輯的基礎。

這除了教科書以外有用嗎?

當然有!幾乎所有專案,只要你有處理不同但類似的物件——從動物園的動物到 UI 元件、事件處理器、文件系統,通通都用得到。

  • 可以寫出通用演算法——你的程式只管抽象的東西,不用管細節。
  • 保證可擴充——你要加一百種新「動物」、「圖形」、「處理器」都不用動舊程式碼。
  • 減少元件之間的耦合(這是面試和架構設計很愛問的重點!)。

2. 基本語法和機制

來回顧一下我們的 AnimalDogCat 類別,順便加強一下多型的概念。既然我們從「虛擬動物園」開始,就繼續進化它吧。

有 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 → Dog, Cat

當你呼叫 animal.MakeSound(),而 Animal 可能裝的是 DogCat,.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 型別的變數上呼叫,但實際上會跑到 CircleRectangle 的方法。在真的圖形編輯器或像 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 myDogAnimal 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();
}

不過通常你這樣寫,代表你的架構有點問題(或你太愛繼承了)。

2
任務
C# SELF, 等級 21, 課堂 3
上鎖
實現基本多型與虛擬方法
實現基本多型與虛擬方法
2
任務
C# SELF, 等級 21, 課堂 3
上鎖
通用的多型物件操作函式
通用的多型物件操作函式
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION