CodeGym /課程 /C# SELF /多型的概念以及它在 OOP 裡的角色

多型的概念以及它在 OOP 裡的角色

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

1. 前言

想像一下:你家裡有電視、音響、冷氣還有智慧燈泡。每個都有自己的遙控器。要開電視,你拿電視的遙控器按 "開啟"。要開音響,你拿音響的遙控器也按同一個 "開啟"。想像一下?超亂對吧?

那如果你有一個 萬用遙控器 呢?你只要按 "開啟",它就會神奇地知道現在要開哪個裝置,然後發送正確的指令。而且這個遙控器根本不知道 電視怎麼開音響怎麼開。它只知道這些裝置都有一個「開啟功能」。

這就是 多型 在程式設計裡的類比啦!

多型

「多型」這個詞來自希臘文 poly(多)和 morph(形狀)。字面意思就是「很多種形狀」。在 OOP 裡,就是一個物件可以有不同的形態,或者更精確地說,同一個方法根據你呼叫它的物件型別,會有不同的行為。

在 C# 裡,多型主要靠繼承還有 virtualoverride 方法來實現。

多型的核心概念就是 向上轉型(upcasting)。這是什麼意思?

回到我們的階層 AnimalDog, Cat。我們知道狗 動物,貓 動物。這種 "is-a" 關係就是繼承的基礎。


public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("某種聲音...");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("汪汪!");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("喵!");
    }
}

因為有 "is-a" 關係,我們可以把衍生類別的物件指定給基底類別的變數。這就叫 向上轉型(upcasting),因為你把物件「升級」成比較通用的型別。C# 編譯器會自動幫你做這件事:


// 我們有一隻狗
Dog myDog = new Dog();

// 可以把 Dog 物件指定給 Animal 型別的變數!
// 這就是向上轉型(upcasting)。
// 右邊是具體的 Dog,左邊是比較通用的 Animal。
Animal generalAnimal = myDog;

// 可以呼叫 MakeSound()!
generalAnimal.MakeSound(); // 輸出 "汪汪!"(不是 "某種聲音...",而是 "汪汪!")               

這裡發生了什麼?

  1. 當你寫 Animal generalAnimal = myDog;,你沒有創造新的動物。你只是把 myDog(其實是 Dog)放進一個標記為 Animal 的「盒子」裡。
  2. 編譯 階段(程式碼變成 byte-code 的時候),generalAnimal 這個變數型別是 Animal。所以編譯器只知道它可以呼叫 Animal 裡有的方法和屬性。
  3. 但最有趣的是:當程式 執行(在 runtime 階段)時,你呼叫 generalAnimal.MakeSound(),.NET 執行環境看的不是變數型別(Animal),而是裡面實際的物件型別(Dog)!而且因為 MakeSound()Animal 標成 virtual,在 Dog 標成 override,所以會呼叫 Dog 的實作。

這種魔法,就是方法的行為由 執行時物件的實際型別 決定,而不是變數型別,這就叫 動態派發執行時多型

想像一個盒子:外面寫著「水果」(這是 Animal 變數型別)。你把蘋果放進去(這是 Dog 物件)。當你說「水果,發出聲音!」(呼叫 MakeSound()),盒子知道裡面是蘋果,就發出「咔嚓」聲,而不是什麼「水果」的通用聲音。

2. 多型實戰展示

最能體會多型的方式,就是實際看它怎麼運作,尤其是你有一堆物件的時候。

來擴充一下我們的「我的小動物園」App。假設我們在動物醫院工作,有各種動物來檢查。我們希望每種動物檢查流程都不一樣,但呼叫檢查的程式碼要很通用。

先在我們的基底類別 Animal 和衍生類別 DogCat 加一個新的虛擬方法 Examine()


public class Animal
{
    public string Name { get; set; }
    public Animal(string name) { Name = name; }
    public virtual void MakeSound() { Console.WriteLine($"{Name} 發出某種聲音..."); }
    public virtual void Examine()
    {
        Console.WriteLine($"檢查 {Name}:");
        Console.WriteLine("  - 檢查呼吸。");
        Console.WriteLine("  - 量體溫。");
    }
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override void MakeSound() { Console.WriteLine($"{Name} 說:汪!"); }
    public override void Examine()
    {
        base.Examine();
        Console.WriteLine("  - 檢查牙齒。");
        Console.WriteLine("  - 狂犬病疫苗。");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void MakeSound() { Console.WriteLine($"{Name} 說:喵!"); }
    public override void Examine()
    {
        base.Examine();
        Console.WriteLine("  - 檢查爪子。");
        Console.WriteLine("  - 驅蟲藥。");
    }
}

現在我們有專屬的 Examine() 方法給狗和貓。注意 base.Examine(); 在 override 方法裡。這樣可以先做基底類別的通用檢查,再加上每種動物自己的步驟。超方便!

現在來想像我們的動物醫院。有一個動物清單,等著檢查:


class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("雷克斯"),
            new Cat("穆卡"),
            new Animal("兔子")
        };

        Console.WriteLine("--- 動物檢查 ---");
        foreach (Animal animal in animals)
        {
            animal.Examine();
            Console.WriteLine();
        }

        Console.WriteLine("--- 聲音時間 ---");
        foreach (Animal animal in animals)
            animal.MakeSound();
    }
}

結果:


--- 動物檢查 ---
檢查 雷克斯:
  - 檢查呼吸。
  - 量體溫。
  - 檢查牙齒。
  - 狂犬病疫苗。

檢查 穆卡:
  - 檢查呼吸。
  - 量體溫。
  - 檢查爪子。
  - 驅蟲藥。

檢查 兔子:
  - 檢查呼吸。
  - 量體溫。

--- 聲音時間 ---
雷克斯 說:汪!
穆卡 說:喵!
兔子 發出某種聲音...

這就是多型的威力! 我們只寫了一個 foreach (Animal animal in animals) 迴圈,每次都呼叫同一個 animal.Examine()(或 animal.MakeSound())。但每次都會跑出 正確、專屬 的方法,不管是狗、貓還是一般動物。程式碼簡單、乾淨又通用,細節都藏在各自的類別裡。

如果明天有一隻新來的 Hamster 倉鼠,只要寫個 Hamster : Animal 類別,override Examine(),它就能直接加入我們的動物檢查系統,foreach 迴圈完全不用改!這就是多型的真正力量。

3. 多型在 OOP 裡的角色

多型不只是個酷炫的詞或程式碼把戲。它是讓你的程式彈性、可擴充、好維護的根本原則。來看看它到底有什麼用:

彈性與擴充性(Open/Closed Principle)

這大概是最重要的優點。多型讓你寫的程式 開放擴充、封閉修改。什麼意思?

  • 開放擴充: 你可以加新動物類型(像 BirdFishHamster),只要繼承 Animal,override 需要的方法就好。
  • 封閉修改: 你不用去改現有操作 Animal[] 的程式碼。那個 foreach 迴圈呼叫 animal.Examine(),不管你加多少新動物都不用動。

想像如果沒有多型,你得寫一堆 if-else ifswitch,每加一種動物就要改一次,超級頭痛。

簡化程式碼

多型讓你操作一堆不同但相關物件的程式碼變超簡單。你不用判斷每個物件型別再呼叫對應方法,只要呼叫基底類別的通用方法,系統會自動幫你找對的實作。

就像你有一個「開門」按鈕,不管是普通門、滑門還是旋轉門,你都不用管細節,只要「開門」,每種門自己會搞定。

抽象化

多型跟抽象化很有關係,這也是 OOP 的一大支柱,之後會細講。它讓你專注在「物件做什麼」(像「發出聲音」、「檢查」),而不是「怎麼做」。你操作的是「動物」這個抽象概念,不是具體的「狗」或「貓」。這樣你可以寫出高層次、乾淨的程式碼,不會被細節綁死。

程式碼重用

雖然繼承本身就能重用基底類別的屬性和方法,多型更進一步。你可以寫出通用的演算法和資料結構(像 Animal[]),可以操作任何繼承自基底類別的物件,不用為每個衍生型別重複寫邏輯。

本質上,多型讓你的程式碼更「專業」,也更能應付需求變動,這在真實開發裡超級重要。

4. 實用小細節

多型類別圖

classDiagram
    class Animal {
        +MakeSound()
    }
    class Dog {
        +MakeSound()
    }
    class Cat {
        +MakeSound()
    }
    class Parrot {
        +MakeSound()
    }

    Animal <|-- Dog
    Animal <|-- Cat
    Animal <|-- Parrot
多型類別階層圖

當你用 MakeSound() 呼叫 Animal 型別的參考時,會呼叫實際物件所屬類別的方法。

多型配合陣列和集合的用法

很常見的需求:遍歷一個異質集合,對每個元素都執行同一套邏輯。


// 在我們的練習 App 裡:
Animal[] zoo = { new Dog("小球"), new Cat("巴西克"), new Parrot("凱莎") };

foreach (Animal animal in zoo)
{
    animal.MakeSound();
}

在真實專案裡,像事件處理、畫圖(每個圖形自己畫)、支付處理(不同卡片和服務)都會這樣用。

多型怎麼運作

物件型別 變數型別 會呼叫哪個方法?
Dog
Animal
Dog.MakeSound()
Cat
Animal
Cat.MakeSound()
Parrot
Animal
Parrot.MakeSound()
Animal
Animal
Animal.MakeSound()

重點是——方法要在基底類別標 virtualabstract,在子類別標 override

5. 多型常見錯誤

現實中學生常常會犯這些錯:

  • 沒把方法標成 virtual,結果多型根本沒發生,永遠只跑一種行為。
  • 搞混:變數該用哪個類別?記住:變數有 型別(像 Animal),你放進去的物件是 實例(像 new Dog())。
  • 想用基底類別變數去呼叫子類別才有的新成員。例如:

Animal pet = new Dog();
pet.Bark(); // 錯誤!Animal 沒有 Bark()

怎麼辦? 如果真的很需要,可以轉型,但最好還是讓程式只用基底類別有的方法。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION