CodeGym /課程 /C# SELF /C# 的虛擬方法:多型與覆寫

C# 的虛擬方法:多型與覆寫

C# SELF
等級 20 , 課堂 2
開放

1. 前言

想像一下,你有個基礎類別 Animal,裡面有個方法 Speak()。預設所有動物都會印出 "某種聲音"。你想要做一些繼承類別,比如 DogCat,讓牠們不要只會說 "某種聲音",而是各自的台詞("汪!""喵!")。

你可以在每個繼承類別裡寫自己的 Speak(),但如果你用 Animal[] 這種集合來操作,呼叫 animal.Speak() 還是只會跑 Animal 的方法,不會跑到狗或貓的版本——因為 C# 預設是看 變數型別,不是背後實際的物件型別。

來個超直白的例子:


Animal dog = new Dog();
dog.Speak(); // 欸,這會印什麼?

預設會是 "某種聲音",不是 "汪!",就算 Dog 有自己的 Speak()。怎麼辦?你要在基礎類別把方法宣告成 virtual,然後在子類別用 override

什麼是虛擬方法?

virtual 方法就是在基礎類別用 virtual 修飾詞宣告的方法,目的是讓繼承類別可以覆寫( override)它。

當你用基礎類別的參考去呼叫這種方法時,會跑到「實際」物件的那個方法——就像現實生活:你養了一隻動物,牠其實是貓,牠就會說 "喵!",不會只會 "某種聲音"

虛擬方法就是 C# 多型的核心。

2. 虛擬方法怎麼宣告

怎麼宣告虛擬方法

在基礎類別用 virtual 關鍵字宣告方法:


public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("某種聲音");
    }
}
在基礎類別宣告虛擬方法

在繼承類別用 override


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

public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("喵!");
    }
}
在繼承類別覆寫虛擬方法

範例:多型展示


// 比如在 Program.cs 檔案

// 基礎類別 Animal,有虛擬方法 Speak
public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("某種聲音");
    }
}

// Dog 類別,覆寫 Speak 行為
public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("汪!");
    }
}

// Cat 類別,也覆寫 Speak
public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("喵!");
    }
}

public static class Program
{
    public static void Main()
    {
        Animal[] animals = new Animal[]
        {
            new Animal(),
            new Dog(),
            new Cat()
        };

        foreach (Animal animal in animals)
        {
            animal.Speak(); // 會印出:"某種聲音", "汪!", "喵!"
        }
    }
}

有看到虛擬方法的魔法了吧?就算變數型別是 Animal,還是會呼叫到實際實作的那個版本。

3. 底層怎麼跑:虛擬表(virtual table)

簡單說明機制

當你把方法宣告成虛擬,編譯器會幫每個型別建一個「虛擬表」來記錄方法(有點像每道菜的菜單)。呼叫這種方法時,C# 不是看變數型別,而是看參考背後的實際物件型別——然後「塞」進去對應的那個方法。

可以說,C# 會自動幫你換成專屬的食譜,如果類別有自己的版本。

比喻:合約可以「加註小條款」

想像你有個租約(基礎類別),裡面寫說承租人預設每月付固定金額(方法 PayRent())。但你這個房東很聰明,允許在附錄裡寫特殊條件(虛擬方法)。如果某個承租人(繼承類別)用這權利,他就會寫自己的 override,之後收費就會變成特別的方式。

4. 什麼時候該用虛擬方法?

  • 如果基礎類別的方法大多數繼承類別都適用,但有些需要自己特別的行為。
  • 當你設計類別繼承結構,預期有些邏輯會被擴充或改寫。
  • 當你要做彈性的架構,比如商業邏輯,不同操作型別可能要有自己的行為。

在實際專案裡,虛擬方法是 模板方法(Template Method)、策略(Strategy)等設計模式的基礎,還有一堆 OOP 的核心。

跟一般方法比較

一般方法 虛擬方法
能不能覆寫 不能 可以(override
呼叫方式 看變數型別 看「實際」物件型別
多型 沒有

常見問題

  • 建構子可以虛擬嗎?
    不行!建構子永遠不能虛擬——它們只會根據物件型別執行。
  • 虛擬方法可以是靜態的嗎?
    不行,只有實例方法能虛擬。靜態方法沒物件,根本沒東西可以覆寫。
  • 欄位可以虛擬嗎?
    不行,只有方法、屬性和事件可以。

5. 實作練習:「動物世界」App 進階

在我們的學習 App 裡,你已經做了一個簡單的動物繼承結構。現在來加點虛擬方法,讓不同動物吃東西的方式都不一樣!

帶註解的程式碼範例


// 在基礎類別加個虛擬方法 Eat
public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("某種聲音");
    }

    public virtual void Eat()
    {
        Console.WriteLine("吃一般食物。");
    }
}

// Dog 覆寫 Eat
public class Dog : Animal
{
    public override void Speak() => Console.WriteLine("汪!");
    public override void Eat() => Console.WriteLine("吃狗飼料。");
}

// Cat 也有自己的行為
public class Cat : Animal
{
    public override void Speak() => Console.WriteLine("喵!");
    public override void Eat() => Console.WriteLine("吃魚。");
}

public static class Program
{
    public static void Main()
    {
        Animal[] animals = new Animal[]
        {
            new Animal(),
            new Dog(),
            new Cat()
        };
        foreach (var animal in animals)
        {
            animal.Speak();
            animal.Eat();
            Console.WriteLine("---");
        }
    }
}

這樣每隻貓貓狗狗都會有自己的吃法啦!而基礎 Animal 還是吃一些很奇怪的東西,完全就是「預設值」的感覺。

6. 常見錯誤與細節

  • 有時會忘記在繼承類別加 override,結果還是跑基礎類別的方法。
  • 或者反過來,忘了在基礎類別加 virtual,那繼承類別就不能寫 override

virtual 不能寫 override

C# 編譯器不會讓你覆寫一個沒宣告成虛擬(或 abstract)的基礎類別方法。

為什麼要 override

override 就是明確告訴編譯器跟看你程式的人:「我不是隨便寫新方法,我是有意識地改掉基礎類別的行為」。這樣可以避免你以為「覆寫」了,其實只是多寫了一個同名方法。

如果忘了 override,直接寫新方法

public class Dog : Animal
{
    public void Speak()
    {
        Console.WriteLine("汪!");
    }
}
這個 不是覆寫!多型不會動作。

這種情況不是覆寫!如果你用 Animal 型別的變數指到 Dog,還是只會跑動物的那個方法("某種聲音"),完全就是型別的陷阱啊!

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