2017年8月28日 星期一

C# - new和override的差異與目的

[Ref] 



C# - new和override的差異與目的
今天在公司,同事在學習C#,剛好學到了繼承這一章節,但是對於new和override並不是很了解,所以就跑來問小弟我,好死不死,剛好問到這個幾百年都沒甚麼在用的東西,所以緊急惡補一下後,決定還是把new和override寫到Blog來好了,以防未來又被問倒…( 至少未來被問到,又忘記的時候可以說,我Blog裡面有寫XDD )。

override

當然,這部分不是這篇文章的重點,相信學過Java或是常寫C#等等OO語言的人,對這個東西應該一點也不陌生了,我們就直接來看看範例吧;下面這個範例其實很簡單,簡單的說,就是Class2繼承Class1,也就是說父類別為Class1,子類別為Class2,換言之Class1為基底類別,Class2為衍生類別( 好吧,我也搞不懂為什麼會出現那麼多名詞,反正就是Class2繼承Class1就是了 ),而有時因為繼承的物件( Class2 )的方法( Test )會和父類別( Class1 )的方法 ( Test )有不一樣的處理邏輯,例如我同事就舉例說,父類別是鳥,子類別是企鵝 ( 至於企鵝到底是不是鳥類,就不在這邊討論了 ),而鳥會有一個飛的方法,可以飛得遠遠的,但是子類別的企鵝,雖然也有飛的方法,但可能只能離地幾公分;總之,子類別的方法名稱雖然可能會和父類別的方法名稱相同,但其實裡面的邏輯不同,而這個時候,我們就可以在父類別的方法裡面加上Virtual,子類別裡面的方法加上override,來進行方法邏輯複寫的動作,如下程式碼。

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Class1
    {
        //如果沒有加上Virtual,子類別又override這個方法
        //編譯器就會生氣喔!
        public virtual void Test()
        {
            Console.WriteLine("Class1.Test()");
        }
    }

    public class Class2 : Class1
    {
        //這裡使用override來複寫
        public override void Test()
        {
            Console.WriteLine("Class2.Test()");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Class2 c = new Class2();
            c.Test();//理所當然會出現Class2.Test()

            //讓畫面可以暫停。
            Console.ReadLine();
        }
    }
}
所以到這邊,畫面上,會出現的是Class2.Test,也就是說,實際上是執行Class2的Test方法。

new

接下來,我們看一下下面這個程式碼,我相信使用者一定不是故意的沒寫override,因為初學者可能不是很懂,小弟以前就幹過這種事情,直接寫了兩個Class,也沒override。

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Class1
    {
        //故意沒加上Virtual
        public void Test()
        {
            Console.WriteLine("Class1.Test()");
        }
    }

    public class Class2 : Class1
    {
        //這裡也不使用override關鍵字來複寫
        public void Test()
        {
            Console.WriteLine("Class2.Test()");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Class2 c = new Class2();
            c.Test();//結果還是會出現Class2.Test()

            //讓畫面可以暫停。
            Console.ReadLine();
        }
    }
}
這時,編譯器就會不爽,如下圖。
ooo隱藏了繼承的成員xxx,如果是刻意要隱藏,請使用new關鍵字。
image
好,雖編譯器不爽,但還是可以過,為什麼呢?,我們可以從MSDN找到這串字
If the method in the derived class is not preceded by new or override keywords, the compiler will issue a warning and the method will behave as if the new keyword were present.
用小弟的破英文,稍微翻一下,意思就是說,假如這個方法沒有new或是override這個兩個關鍵字,偉大的編譯器,就會很不爽地提醒你,但還是會很撈叨的幫你加上new這個關鍵字的效果。
所以就算不加上new和override還是可以正常編譯,就是因為偉大的編譯器幫你處理了,但沒加上new這個關鍵字,編譯器還是會很擔心,怕使用者不知道它會自動地加上這個關鍵字,所以會冒出這個訊息來告知使用者"你現在的父類別方法xxx會隱藏起來,而實際會使用子類別的ooo方法,如果你真的是要這樣的效果,請加上關鍵字new,來讓編譯器的我知道";聽起來還不錯啊,編譯器其實不是不爽,而是關心你!,但目前new關鍵字和override關鍵字,看起來的效果一樣阿,所以未來我們就可以不用加上new和override了嗎!?畢竟加上new和override,看起來效果都差不多阿?,我承認我也有這樣想過XDD,new和override雖然目前看起來的結果都一樣,但真正的涵義是不同的,不然就不會出現這篇文章了XDD。

隱藏和複寫

為什麼那個警告是要用隱藏這字眼,而不是複寫這詞呢?,我們先看一下下面的程式碼,這也是今天同事問我的問題,我們一樣有Class1和Class2,但是在Class1上多加一個PreTest的方法,並在這個方法裡面呼叫Test(),而Class2裡面沒有寫PreTest方法,只有利用new關鍵字,來將Class1的Test方法隱藏起來,但畢竟Class2是繼承Class1,所以實際上在執行時期,還是可以執行PreTest方法。

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Class1
    {
        //多加一個方法
        public void PreTest()
        {
            Console.WriteLine("PreTest()");
            Test();
        }
        //
        public void Test()
        {
            Console.WriteLine("Class1.Test()");
        }
    }

    public class Class2 : Class1
    {
        //使用new關鍵字,讓編譯器開心一點。
        public new void Test()
        {
            Console.WriteLine("Class2.Test()");
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            
            Class2 c = new Class2();
            //改成呼叫c.PreTest()
            c.PreTest();//出現卻是Class1.Test()!!?

            //讓畫面可以暫停。
            Console.ReadLine();
        }
    }
}
好,結果出來了,答案卻是Class1.Test(),這的確讓我很錯愕XDD,原本想說,這是Class2的實體,所以雖然透過Class1的PreTest來呼叫Test方法,但實際上應該也是要呼叫到Class2的Test方法阿,怎麼會是呼叫到Class1的Test方法呢!?答案很簡單,就出在new這個關鍵字;我們回過頭來看看,為什麼編譯器警告時要用隱藏這個字眼,就是因為new的情況,只是會把Class1的Test隱藏起來,但實際上,無論是Class1或是Class2都還存著各自的Test方法;而C#的機制,當繼承的Class2並無特別的撰寫PreTest方法,所以會回到Class1裡面去執行PreTest,當執行到Test()這行的時候,又因為Class2不是使用Override的方式來複寫Class1的Test方法(也就是說實際上存著兩個Test方法),所以就會直接使用Class1的方法;所以如果真的要執行Class2的Test方法,就必須要使用Override,也就是複寫真正的含意,如下。

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Class1
    {
        //多加一個方法
        public void PreTest()
        {
            Console.WriteLine("PreTest()");
            Test();
        }
        //
        public virtual void Test()
        {
            Console.WriteLine("Class1.Test()");
        }
    }

    public class Class2 : Class1
    {
        //使用override
        public override void Test()
        {
            Console.WriteLine("Class2.Test()");
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            
            Class2 c = new Class2();
            //改成呼叫c.PreTest()
            c.PreTest();//出現的是Class2.Test()

            //讓畫面可以暫停。
            Console.ReadLine();
        }
    }
}
這就是override和new最大的差異點。

多型後的override

這時候,可能因為讀到後面章節,教到多型了 ( 如果都很懂得大大們,請體諒這些梗 ),所以我們就使用多型的方式,修改一下程式碼,並使用override,如下。

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Class1
    {
        //因為要使用override所以加上Virtual
        public virtual void Test()
        {
            Console.WriteLine("Class1.Test()");
        }
    }

    public class Class2 : Class1
    {
        //使用override關鍵字來複寫
        public override void Test()
        {
            Console.WriteLine("Class2.Test()");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            //我們用個多型吧!
            Class1 c = new Class2();
            c.Test();//結果並不易外,出現的是Class2.Test()

            //讓畫面可以暫停。
            Console.ReadLine();
        }
    }
}
這邊當然不意外的是出現Class2.Test,因為Class2複寫了Class1的Test方法,所以呼叫c.Test()的時候,因為知道Class2有加上override關鍵字,所以實際是呼叫Class2的Test方法。

多形後的new

現在,我們在多形,並配合new,如下程式碼。

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Class1
    {
        //使用new,就可以不加上virtual
        public void Test()
        {
            Console.WriteLine("Class1.Test()");
        }
    }

    public class Class2 : Class1
    {
        //使用new關鍵字
        public new void Test()
        {
            Console.WriteLine("Class2.Test()");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            //我們用個多型吧!
            Class1 c = new Class2();
            c.Test();//結果超易外,出現的是Class1.Test()

            //讓畫面可以暫停。
            Console.ReadLine();
        }
    }
}
嚇到了吧,小弟我第一次看的確嚇到XDD,這次出來的結果又不是原本預期的Class2.Test(),就如前面說的,new只是會隱藏Class1的Test,所以又會存在著兩個Test方法,而C#預設的情況下,會先處理變數型別的方法,例如上面的程式碼,變數型別為Class1,所以預設如果執行c.Test(),則會進行Class1的Test方法,除非Class2的Test有明確的加上override關鍵字,這時候,才會進行Class2的Test方法。

為什麼要搞得那麼複雜

講到這邊,差不多就把new和override可能發生的狀況都講過一次了,但為何C#要把事情弄得那麼複雜,其實凡事必有因,會有這樣的機制,是為了擴充上的彈性;假設目前的架構是Class1裡面沒有任何方法,而Class2裡面有寫一個Test方法,過了一段時間,可能Class1也加上了Test方法,這時候Class1和Class2也都有了Test方法了,此時,就算編譯,依然是沒有任何的問題的,如下。

c1.Test();
Class2 c2 = new Class2();//不影響,還是呼叫Class2的Test
c2.Test();
Class1 c3 = new Class2();//不影響,在Class1加上Test之前,是不可能這樣寫的
c3.Test();
Class2 c4 = new Class1();//怎麼可能會有這種寫法勒...
也因為這樣,所以C#才會有new和override的一個差異,不過現實上,小弟我還是很少用到new就是了。

結論

寫了那麼多東西,基本的原則就是,如果是override,就和一般的繼承沒甚麼兩樣,但若是用到new,會先看變數型別來決定呼叫的是哪裡的方法,如果這個變數型別裡面的方法是繼承而來的,就會回到父層去執行,而如果父層裡面還有呼叫其他方法,只要子層沒有override,則都會執行父層的方法。

後記

這篇寫了整整三個小時,重新編排了三次= =,目的也是希望能看得比較懂XDD,這個東西也是C#很眉眉角角的東西,現實生活上,應該是很少用到new,但也難講有哪天會遇到這種的設計方式,所以在這邊紀錄一下,希望未來忘記,還有地方可以找的到XDD。



C#] 利用 interface(介面) abstract(抽象) override(覆寫) inherit(繼承) demo



[Ref]


來源:
  1. (原創) interface和abstract class有何不同? (C/C++) (.NET) (C#)
  2. [C#] abstract 和 virtual 函數的差異




  1. 建立一個abstract的Door類別,裡面有Open(開門)與Close(關門)這兩個虛擬方法可以用

     
  2. 建立一個HorizontalDoor類別,它繼承了原先的Door,所以它也有Open與Close這兩個方法可以用

     
  3. 建立一個VerticalDoor類別,它也繼承了Door,可是我在裡面override了原先的Open與Close,另外實作其方法

     
  4. 建立一個IAlarm介面,裡面定義了Alert這個方法

     
  5. 建立一個Alarm類別,它繼承了IAlarm,並在裡面實作Alert

     
  6. 建立一個AlarmDoor類別,它繼承了Door,同時在裡面使用了Alarm類別的Alert方法

     
  7. 建立一個AutoAlarmDoor類別,它繼承了AlarmDoor,並覆寫了原本的Open方法,裡面呼叫base.Open方法並接著呼叫Alert方法

     
  8. 建立一個DoorController類別,這個類別是用來控制管理所有的Door

     
  9. 完成 !!

     
  10. 執行結果
    2016-09-07 23_06_31-file____C__Users_Minsheng_Desktop_ConsoleApplication_Door_ConsoleApplication_Doo

  • 注意事項
    1. abstract method 不會有程式內容
    2. abstract method 繼承後,一定要 override
    3. virtual method 一定要有程式內容
    4. 宣告為 virtual 的 method,繼承後才可以進行 override
    5. 設定為 virtual 的 method,沒有一定要 override