2017年9月7日 星期四

.NET C# 類別 class 的繼承 Inheritance 與方法的覆寫 override 和覆蓋 new 的用法

在我們使用物件來設計不同實體的狀態與行為的時候,有個最為重要的 OOP 特性,那就是繼承 Inheritance;透過了繼承,我們可以將基底類別 Base Class的成員,全部都繼承到衍生類別內 Derived Class,也就是說,我們可以直接使用在基底類別中所宣告與定義的各類成員。


了解更多關於 [C# 和 .NET 中的繼承的使用方式
了解更多關於 [C# 程式設計手冊 



如下面的程式碼,是我們這次要練習的兩個主要類別
  • BaseClass
    這個類別是我們的基底類別
  • DerivedClass
    他繼承了 BaseClass 類別,因此,在 DerivedClass 內,是可以直接使用這個成員屬性 MyProperty。
        class BaseClass
        {
            public string MyProperty { get; set; }
        }
        class DerivedClass : BaseClass
        {
        }
    
不過,很多時候,當我們從基底類別繼承該類別到我們新建立的衍生類別時,有些行為我們需要在衍生類別內進行重新設計與定義,這樣才能夠滿足我們的需求;面對這樣的需求,您將會有兩個選擇,一個就是使用 OOP 的其中一個特色,多型 Polymorphism,這個時候,你需要在衍生類別中同樣函式簽章的前面,加入關鍵字 override ,就可以做到這樣的功能,不過,您也需要在基礎類別的同名同簽章的函式前面,加入 virtual 關鍵字。
另外一種選擇,就是直接在衍生類別類的函式前面,直接加入關鍵字 new,表示在衍生類別內,這個函式將會被覆蓋成為全新方法。
因此,為了要在同一個 C# 原始檔案中,使用同樣的基底與衍生類別名稱,並且也要能夠表達出 覆載 override與複寫 new 的差異,所以,在這裡我們使用了命名空間 namespace來做出這樣的區隔效果。

繼承類別的定義

在這裡,我們使用了三個命名空間,分別定義出同樣名稱的基底與衍生類別與繼承關係,但是,使用了覆載與副寫和沒有這兩用法的類別,分別做了同樣的程式碼呼叫,讓我們看看會出現甚麼結果。
  • 沒有虛擬的類別關係
    在這個命名空間內,這兩個類別類,並沒有同樣名稱與同樣函式簽章的定義。
  • 有虛擬的類別關係
    在這個命名空間內,基底類別的方法 MyMethod1() 有標示著虛擬 Virtual 關鍵字,並且在衍生類別中,也有同樣名稱與函式簽章的 MyMethod1(),不過,在該函式前面有使用了 override 關鍵字,這表示系統會在執行時期 runtime,動態的根據物件變數的實際型別,動態決定要呼叫哪個 MyMethod1() 函式。
  • 有覆蓋的類別關係
    在這個命名空間內,分別在基礎類別與衍生類別內,都有同樣名稱與函式簽章的函式,不過,在這裡,衍生類別裡的 MyMethod1() 這個函式,使用了 new 關鍵字,表示在衍生類別裡面的這個函式,是個全新的函式。
    namespace 沒有虛擬的類別關係
    {
        class BaseClass
        {
            public string MyProperty { get; set; }
            public void MyMethod1()
            {
                Console.WriteLine($"呼叫 BaseClass.MyMethod1() 方法");
            }
        }
        class DerivedClass : BaseClass
        {
            public void MyMethod2()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod2() 方法");
            }
        }
    }

    namespace 有虛擬的類別關係
    {
        class BaseClass
        {
            public string MyProperty { get; set; }
            public virtual void MyMethod1()
            {
                Console.WriteLine($"呼叫 BaseClass.MyMethod1() 方法");
            }
        }
        class DerivedClass : BaseClass
        {
            public override void MyMethod1()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod1() 方法");
            }
            public void MyMethod2()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod2() 方法");
            }
        }
    }

    namespace 有覆蓋的類別關係
    {
        class BaseClass
        {
            public string MyProperty { get; set; }
            public void MyMethod1()
            {
                Console.WriteLine($"呼叫 BaseClass.MyMethod1() 方法");
            }
        }
        class DerivedClass : BaseClass
        {
            public new void MyMethod1()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod1() 方法");
            }
            public void MyMethod2()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod2() 方法");
            }
        }
    }

開始進行測試

首先,我們要先進行沒有同樣函式名稱與函式簽章的繼承關係類別的使用,在這裡,我們會分別產生三個物件,為了要能夠區分這個測試程式中有同樣的名稱的類別,所以,我們將會在這些基底類別與衍生類別的前面,分別加入了特定的命名空間 namespace。
  • BaseClassBaseObject
    這個物件將是屬於基底類別的型別,不過,實際建立的物件將會是基底類別的物件
  • DerivedClassDerivedObject
    這個物件將是屬於衍生類別的型別,不過,實際建立的物件將會是衍生類別的物件
  • BaseClassDerivedObject
    這個物件將是屬於基底類別的型別,不過,實際建立的物件將會是衍生類別的物件
        private static void 測試_沒有虛擬的類別關係()
        {
            沒有虛擬的類別關係.BaseClass BaseClassBaseObject = new 沒有虛擬的類別關係.BaseClass();
            沒有虛擬的類別關係.DerivedClass DerivedClassDerivedObject = new 沒有虛擬的類別關係.DerivedClass();
            沒有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 沒有虛擬的類別關係.DerivedClass();

            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();");
            BaseClassBaseObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            DerivedClassDerivedObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();");
            DerivedClassDerivedObject.MyMethod2();
            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            BaseClassDerivedObject.MyMethod1();
            // 為什麼底下這行無法執行呢?
            // BaseClassDerivedObject 這個物件看不到 MyMethod2() 方法
            //BaseClassDerivedObject.MyMethod2();
        }
接著,我們將會實際執行這三個實體物件的 MyMethod1、MyMethod2 兩個方法,我們可以從執行結果中看到我們所預期的執行結果。也就是所執行的方法,就是該實體物件內所擁有定義的方法,而在 DerivedClassDerivedObject.MyMethod1(); 與 BaseClassDerivedObject.MyMethod1(); 這些敘述中,其當時的物件實際上是衍生類別產生的執行個體 Instance (不管當時該物件變數所宣告的型別是基底類別還是衍生類別),不過,因為,在這個衍生類別中,並沒有宣告 MyMethod1,因此,此時將會使用從基底類別中繼承來的 MyMethod1,故,您看到所執行的 MyMethod1方法,將是定義在基底類別中的 MyMethod1。
這裡使用的是繼承的觀念
物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();
呼叫 DerivedClass.MyMethod2() 方法
物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
Press any key for continuing...
第二個測試程式碼,將會使用命名空間 有虛擬的類別關係 裡面所定義的類別,測試程式碼如下:
        private static void 測試_有虛擬的類別關係()
        {
            有虛擬的類別關係.BaseClass BaseClassBaseObject = new 有虛擬的類別關係.BaseClass();
            有虛擬的類別關係.DerivedClass DerivedClassDerivedObject = new 有虛擬的類別關係.DerivedClass();
            有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 有虛擬的類別關係.DerivedClass();

            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();");
            BaseClassBaseObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            DerivedClassDerivedObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();");
            DerivedClassDerivedObject.MyMethod2();
            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            BaseClassDerivedObject.MyMethod1();
        }
在這個測試中,基底類別中有定義了虛擬 virtual MyMethod1, 而衍生類別中,使用了 override MyMethod1 來覆寫了這個函式的定義。
所以,當這個敘述 DerivedClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別也是衍生類別),所以,在執行時期,會自動決定要使用衍生類別內複寫的 MyMethod1() 方法。
而這個敘述 BaseClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別卻是基底類別),所以,在執行時期,會自動決定要使用衍生類別內複寫的 MyMethod1() 方法(因為有使用了 virtual 與 override 機制,並不會因為當時物件變數宣告的型別為基底類別,就會去執行基底類別的方法)。
這裡使用了 OOP 的多型 Polymorphism 的觀念
物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();
呼叫 DerivedClass.MyMethod2() 方法
物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
Press any key for continuing...
第三個測試程式碼,將會使用命名空間 有覆蓋的類別關係 裡面所定義的類別,測試程式碼如下:
        private static void 有覆蓋的類別關係()
        {
            有虛擬的類別關係.BaseClass BaseClassBaseObject = new 有虛擬的類別關係.BaseClass();
            有虛擬的類別關係.DerivedClass DerivedClassDerivedObject = new 有虛擬的類別關係.DerivedClass();
            有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 有虛擬的類別關係.DerivedClass();

            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();");
            BaseClassBaseObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            DerivedClassDerivedObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();");
            DerivedClassDerivedObject.MyMethod2();
            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            BaseClassDerivedObject.MyMethod1();
        }
在這個測試中,基底類別中有定義了 MyMethod1, 而衍生類別中,使用了 new 關鍵字,重新宣告了一個全新屬於衍生類別的 MyMethod1。
所以,當這個敘述 DerivedClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別也是衍生類別),所以,理所當然的,將會執行衍生類別所定義的 MyMethod1 這個方法。

而這個敘述 BaseClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別卻是基底類別),因為在衍生類別中使用了 new 來重新宣告了一個全新的 MyMethod1 方法,所以,會根據當時的物件型別,直接使用了衍生類別中所定義的 MyMethod1 方法。
物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();
呼叫 DerivedClass.MyMethod2() 方法
物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
Press any key for continuing...

了解更多關於 [C# 和 .NET 中的繼承的使用方式
了解更多關於 [C# 程式設計手冊 



1 則留言:

  1. 看到您的分享寫得很詳盡
    但最後一點跟我測試的結果有點不同
    有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 有虛擬的類別關係.DerivedClass();
    Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
    BaseClassDerivedObject.MyMethod1();
    結果會是
    物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
    呼叫 BaseClass.MyMethod1() 方法
    看起來應該是如果用new 會依據物件的變數型別決定要執行哪個成員 而不是實體物件的型別

    回覆刪除