[C#] Virtual 修飾詞使用心得筆記

紀錄 Virtual 修飾詞使用心得筆記與虛方法應用範例

寫這篇目的就是把物件導向繼承概念裡這個 Virtual 修飾詞使用做一個整理,順帶對照一下名詞說明~

網路上相關文章很多,但是看完大都還是很矇的!主要是繁體/簡體中文對名詞的說法真的是很豐富…

套句博主說『其實如果能很輕易看懂這類文章的人,大概都是已經會了的人』~對我這種容易失憶又入門的使用者需要的是適當說明加上合理的範例才容易消化理解!


首先先給以下名詞做一些統一的中文說法,方便之後解說理解是一致的

  • Virtual Method - 虛方法(有些叫做虛擬方法)
  • Override 修飾詞 - 覆寫
  • New 修飾詞 - 隱藏/覆蓋
  • Base Class - 基礎類別
  • Derived Class - 衍生類別
  • Declare - 宣告
  • Instance - 實體

Virtual 使用上就是兩個地方需要加上修飾詞

  1. 基礎類別要被繼承的方法前加上 virtual 修飾詞
  2. 衍生類別要覆寫的方法前加上 override 修飾詞(或是使用隱藏 new)

這裡要注意的是,當父類別使用了 virtual 之後,繼承的子類別宣告同名同型態方法沒加上 override 或是 new 編譯器是會報錯!

那父類別中方法連 virtual 都不加,那會怎麼樣呢?……..編譯器還是報錯,可是可以執行,貼心的幫忙加上 new

所以正常情況下,應該會有以下兩種使用方式

一、覆寫 Override

public class Vehicle
{
    public virtual void Run()
    {
        Console.WriteLine("Base Class - Vehicle Run!");
    }
}

public class Car : Vehicle
{
    public override void Run()
    {
        Console.WriteLine("Derived Class - Car Run!");
    }
}
Vehicle v = new Car();
v.Run();

輸出:『Derived Class - Car Run!』

二、隱藏 Hiding

public class Vehicle
{
    public virtual void Run()
    {
        Console.WriteLine("Base Class - Vehicle Run!");
    }
}

public class Car : Vehicle
{
    public new void Run()
    {
        Console.WriteLine("Derived Class - Car Run!");
    }
}
Vehicle v = new Car();
v.Run();

輸出:『Base Class - Vehicle Run!』

由以上兩個例子,可以明顯看出使用 override 與 new 兩者差異點,可以看出正常情況下根本不會用 new 也沒必要用 new,衍生類別宣告了一個同名同態的方法不就是要覆寫了嗎?

這邊要特別說明的是,兩個例子中的最後是用宣告基礎類別去引用實體化衍生類別,這是 C# 可允許的情況,尤其是當使用 interface 來達到依賴反轉時常用的手法!

例子中的這段『Vehicle v = new Car();』 Vehicle 可以稱為宣告類別,Car 稱之為實體類別

這裡整理一下虛方法的檢查判斷執行流程:

  1. 當調用一個物件的方法時,系統會直接去檢查這個物件宣告定義的類別,即宣告類別,看所調用的方法是否為虛方法
  2. 如果不是虛方法,那麼它就直接執行該方法;而如果有 virtual 關鍵字,也就是一個虛方法時,就不會立刻執行該方法,而是轉去檢查物件的實體類別
  3. 檢查這個實體類別的定義中是否有覆寫虛方法(通過 override 修飾詞),如果是有,馬上執行該實體類別中覆寫的方法;而如果沒有,系統就會不停地往上找實體類別的父類別,並對父類別重複剛才在實體類別裡的檢查,直到找到第一個覆寫了該虛方法的父類別為止,然後執行該類別裡覆寫方法

為了更好理解上述判斷流程,這裡用個比較宏大的例子:

public class A
{
    public virtual void Func() // 注意 virtual, 表示這是一個虛方法
    {
        Console.WriteLine("Class A Func");
    }
}
class B : A // 注意 B 是從 A 類別繼承, 所以 A 是父類別, B 是子類別
{
    public override void Func() // 注意 override, 表示重新覆寫虛函數
    {
        Console.WriteLine("Class B Func");
    }
}
class C : B // 注意 C 是從 B 類別繼承, 所以 B 是父類別, C 是子類別
{
}
class D : A // 注意 D 是從 A 類別繼承, 所以 A 是父類別, D 是子類別
{
    public new void Func() // 注意 new,表示覆蓋父類別裡的同名方法,而不是重新覆寫
    {
        Console.WriteLine("Class D Func");
    }
}
A a;         // 定義一個 a 這個 A 類別的物件, 這個 A 就是 a 的宣告類別
A b;         // 定義一個 b 這個 A 類別的物件, 這個 A 就是 b 的宣告類別
A c;         // 定義一個 c 這個 A 類別的物件, 這個 A 就是 c 的宣告類別
A d;         // 定義一個 d 這個 A 類別的物件, 這個 A 就是 d 的宣告類別

a = new A(); // 實體化 a 物件, A 是 a 的實體類別
b = new B(); // 實體化 b 物件, B 是 b 的實體類別
c = new C(); // 實體化 c 物件, C 是 c 的實體類別
d = new D(); // 實體化 d 物件, D 是 d 的實體類別

a.Func();
// 執行a.Func
// 1.先檢查宣告類別 A
// 2.檢查到是虛方法
// 3.轉去檢查實體類別 A,為虛方法本身
// 4.執行實體類別 A 中的方法
// 5.輸出結果『Class A Func』

b.Func();
// 執行b.Func
// 1. 先檢查宣告類別 A
// 2. 檢查到是虛方法
// 3. 轉去檢查實體類別 B,有覆寫的
// 4. 執行實體類別 B 中的方法
// 5. 輸出結果『Class B Func』

c.Func();
// 執行c.Func
// 1. 先檢查宣告類別 A
// 2. 檢查到是虛方法
// 3. 轉去檢查實體類別 C,無覆寫的
// 4. 轉去檢查類別 C 的父類別 B, 有覆寫的方法
// 5. 執行父類別 B 中的Func方法 
// 6. 輸出結果『Class B Func』

d.Func();
// 執行d.Func
// 1. 先檢查宣告類別 A
// 2. 檢查到是虛方法
// 3. 轉去檢查實體類別 D,無覆寫的(這個地方要注意了,雖然 D 裡有實現Func(),但沒有使用override關鍵字,所以不會被認為是覆寫)
// 4. 轉去檢查類別 D 的父類別 A,為虛方法本身
// 5. 執行父類 A 中的Func方法
// 6. 輸出結果 『Class A Func』

D d1 = new D();
d1.Func();
// 執行D類別裡的Func(),  輸出結果『Class D Func』

最後一個例子參透了,就是對 virtal 使用的完滿了,來個完結灑花吧~