1. 程式人生 > >C#多型(虛方法,抽象,介面實現)

C#多型(虛方法,抽象,介面實現)

轉自   淺談C# 多型的魅力(虛方法,抽象,介面實現)

前言:我們都知道面向物件的三大特性:封裝,繼承,多型。封裝和繼承對於初學者而言比較好理解,但要理解多型,尤其是深入理解,初學者往往存在有很多困惑,為什麼這樣就可以?有時候感覺很不可思議,由此,面向物件的魅力體現了出來,那就是多型,多型用的好,可以提高程式的擴充套件性。常用的設計模式,比如簡單工廠設計模式,核心就是多型。

其實多型就是:允許將子類型別的指標賦值給父類型別的指標。也就是同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。在執行時,可以通過指向基類的指標,來呼叫實現派生類中的方法。如果這邊不理解可以先放一放,先看下面的事例,看完之後再來理解這句話,就很容易懂了。
理解多型之前首先要對面向物件的里氏替換原則和開放封閉原則有所瞭解。

里氏替換原則(Liskov Substitution Principle):派生類(子類)物件能夠替換其基類(超類)物件被使用。通俗一點的理解就是“子類是父類”,舉個例子,“男人是人,人不一定是男人”,當需要一個父類型別的物件的時候可以給一個子類型別的物件;當需要一個子類型別物件的時候給一個父類型別物件是不可以的!

開放封閉原則(Open Closed Principle):封裝變化、降低耦合,軟體實體應該是可擴充套件,而不可修改的。也就是說,對擴充套件是開放的,而對修改是封閉的。因此,開放封閉原則主要體現在兩個方面:對擴充套件開放,意味著有新的需求或變化時,可以對現有程式碼進行擴充套件,以適應新的情況。對修改封閉,意味著類一旦設計完成,就可以獨立完成其工作,而不要對類進行任何修改。

對這兩個原則有一定了解之後就能更好的理解多型。

首先,我們先來看下怎樣用虛方法實現多型

我們都知道,喜鵲(Magpie)、老鷹(Eagle)、企鵝(Penguin)都是屬於鳥類,我們可以根據這三者的共有特性提取出鳥類(Bird)做為父類,喜鵲喜歡吃蟲子,老鷹喜歡吃肉,企鵝喜歡吃魚。

建立基類Bird如下,新增一個虛方法Eat():

複製程式碼
    /// <summary>
    /// 鳥類:父類
    /// </summary>
    public class Bird
    {
        
/// <summary> /// 吃:虛方法 /// </summary> public virtual void Eat() { Console.WriteLine("我是一隻小小鳥,我喜歡吃蟲子~"); } }
複製程式碼

建立子類Magpie如下,繼承父類Bird,重寫父類Bird中的虛方法Eat():

複製程式碼
    /// <summary>
    /// 喜鵲:子類
    /// </summary>
    public  class Magpie:Bird
    {
        /// <summary>
        /// 重寫父類中Eat方法
        /// </summary>
        public override void Eat()
        {
            Console.WriteLine("我是一隻喜鵲,我喜歡吃蟲子~");
        }
    }
複製程式碼

建立一個子類Eagle如下,繼承父類Bird,重寫父類Bird中的虛方法Eat():

複製程式碼
    /// <summary>
    /// 老鷹:子類
    /// </summary>
    public  class Eagle:Bird
    {
        /// <summary>
        /// 重寫父類中Eat方法
        /// </summary>
        public override void Eat()
        {
            Console.WriteLine("我是一隻老鷹,我喜歡吃肉~");
        }
    }
複製程式碼

建立一個子類Penguin如下,繼承父類Bird,重寫父類Bird中的虛方法Eat():

複製程式碼
    /// <summary>
    /// 企鵝:子類
    /// </summary>
    public  class Penguin:Bird
    {
        /// <summary>
        /// 重寫父類中Eat方法
        /// </summary>
        public override void Eat()
        {
            Console.WriteLine("我是一隻小企鵝,我喜歡吃魚~");
        }
    }
複製程式碼

到此,一個基類,三個子類已經建立完畢,接下來我們在主函式中來看下多型是怎樣體現的。

複製程式碼
    static void Main(string[] args)
    {
        //建立一個Bird基類陣列,新增基類Bird物件,Magpie物件,Eagle物件,Penguin物件
        Bird[] birds = { 
                       new Bird(),
                       new Magpie(),
                       new Eagle(),
                       new Penguin()
        };
        //遍歷一下birds陣列
        foreach (Bird bird in birds)
        {
            bird.Eat();
        }
        Console.ReadKey();
    }
複製程式碼

執行結果:

由此可見,子類Magpie,Eagle,Penguin物件可以賦值給父類物件,也就是說父類型別指標可以指向子類型別物件,這裡體現了里氏替換原則。

父類物件呼叫自己的Eat()方法,實際上顯示的是父類型別指標指向的子類型別物件重寫父類Eat後的方法。這就是多型。

多型的作用到底是什麼呢?
其實多型的作用就是把不同的子類物件都當作父類來看,可以遮蔽不同子類物件之間的差異,寫出通用的程式碼,做出通用的程式設計,以適應需求的不斷變化。
以上程式也體現了開放封閉原則,如果後面的同事需要擴充套件我這個程式,還想再新增一個貓頭鷹(Owl),很容易,只需要新增一個Owl類檔案,繼承Bird,重寫Eat()方法,新增給父類物件就可以了。至此,該程式的擴充套件性得到了提升,而又不需要檢視原始碼是如何實現的就可以擴充套件新功能。這就是多型帶來的好處。

我們再來看下利用抽象如何來實現多型

還是剛才的例子,我們發現Bird這個父類,我們根本不需要使用它建立的物件,它存在的意義就是供子類來繼承。所以我們可以用抽象類來優化它。
我們把Bird父類改成抽象類,Eat()方法改成抽象方法。程式碼如下:

複製程式碼
    /// <summary>
    /// 鳥類:基類
    /// </summary>
    public abstract class Bird
    {
        /// <summary>
        /// 吃:抽象方法
        /// </summary>
        public abstract void Eat();
    }
複製程式碼

抽象類Bird內新增一個Eat()抽象方法,沒有方法體。也不能例項化。
其他類Magpie,Eagle,Penguin程式碼不變,子類也是用override關鍵字來重寫父類中抽象方法。
Main主函式中Bird就不能建立物件了,程式碼稍微修改如下:

複製程式碼
        static void Main(string[] args)
        {
            //建立一個Bird基類陣列,新增 Magpie物件,Eagle物件,Penguin物件
            Bird[] birds = { 
                           new Magpie(),
                           new Eagle(),
                           new Penguin()
            };
            //遍歷一下birds陣列
            foreach (Bird bird in birds)
            {
                bird.Eat();
            }
            Console.ReadKey();
        }
複製程式碼

執行結果:

由此可見,我們選擇使用虛方法實現多型還是抽象類抽象方法實現多型,取決於我們是否需要使用基類例項化的物件.

比如說 現在有一個Employee類作為基類,ProjectManager類繼承自Employee,這個時候我們就需要使用虛方法來實現多型了,因為我們要使用Employee建立的物件,這些物件就是普通員工物件。
再比如說 現在有一個Person類作為基類,Student,Teacher 類繼承Person,我們需要使用的是Student和Teacher建立的物件,根本不需要使用Person建立的物件,
所以在這裡Person完全可以寫成抽象類。

總而言之,是使用虛方法,或者抽象類抽象方法實現多型,視情況而定,什麼情況?以上我說的兩點~

接下來~~~~

我要問一個問題,喜鵲和老鷹都可以飛,這個飛的能力,我怎麼來實現呢?

XXX答“在父類Bird中新增一個Fly方法不就好了~~”

我再問“好的,照你說的,企鵝繼承父類Bird,但是不能企鵝不能飛啊,這樣在父類Bird中新增Fly方法是不是不合適呢?”

XXX答“那就在能飛的鳥類中分別新增Fly方法不就可以了嗎?”

對,這樣是可以,功能完全可以實現,可是這樣違背了面向物件開放封閉原則,下次我要再擴充套件一個鳥類比如貓頭鷹(Owl),我還要去原始碼中看下Fly是怎麼實現的,然後在Owl中再次新增Fly方法,相同的功能,重複的程式碼,這樣是不合理的,程式也不便於擴充套件;

其次,如果我還要新增一個飛機類(Plane),我繼承Bird父類,合適嗎?

很顯然,不合適!所以我們需要一種規則,那就是介面了,喜鵲,老鷹,飛機,我都實現這個介面,那就可以飛了,而企鵝我不實現這個介面,它就不能飛~~

好,接下來介紹一下介面如何實現多型~

新增一個介面IFlyable,程式碼如下:

複製程式碼
    /// <summary>
    /// 飛 介面
    /// </summary>
    public interface IFlyable
    {
        void Fly();
    }
複製程式碼

喜鵲Magpie實現IFlyable介面,程式碼如下:

複製程式碼
    /// <summary>
    /// 喜鵲:子類,實現IFlyable介面
    /// </summary>
    public  class Magpie:Bird,IFlyable
    {
        /// <summary>
        /// 重寫父類Bird中Eat方法
        /// </summary>
        public override void Eat()
        {
            Console.WriteLine("我是一隻喜鵲,我喜歡吃蟲子~");
        }
        /// <summary>
        /// 實現 IFlyable介面方法
        /// </summary>
        public void Fly()
        {
            Console.WriteLine("我是一隻喜鵲,我可以飛哦~~");
        }
    }
複製程式碼

老鷹Eagle實現IFlyable介面,程式碼如下:

複製程式碼
    /// <summary>
    /// 老鷹:子類實現飛介面
    /// </summary>
    public  class Eagle:Bird,IFlyable
    {
        /// <summary>
        /// 重寫父類Bird中Eat方法
        /// </summary>
        public override void Eat()
        {
            Console.WriteLine("我是一隻老鷹,我喜歡吃肉~");
        }

        /// <summary>
        /// 實現 IFlyable介面方法
        /// </summary>
        public void Fly()
        {
            Console.WriteLine("我是一隻老鷹,我可以飛哦~~");
        }
    }
複製程式碼

在Main主函式中,建立一個IFlyable介面陣列,程式碼實現如下:

複製程式碼
    static void Main(string[] args)
    {
        //建立一個IFlyable介面陣列,新增 Magpie物件,Eagle物件
        IFlyable[] flys = { 
                       new Magpie(),
                       new Eagle()
        };
        //遍歷一下flys陣列
        foreach (IFlyable fly in flys)
        {
            fly.Fly();
        }
        Console.ReadKey();
    }
複製程式碼

執行結果:


由於企鵝Penguin沒有實現IFlyable介面,所以企鵝不能物件不能賦值給IFlyable介面物件,所以企鵝,不能飛~

好了,剛才我提到了飛機也能飛,繼承Bird不合適的問題,現在有了介面,這個問題也可以解決了。如下,我新增一個飛機Plane類,實現IFlyable介面,程式碼如下:

複製程式碼
    /// <summary>
    /// 飛機類,實現IFlyable介面
    /// </summary>
    public  class Plane:IFlyable
    {
        /// <summary>
        /// 實現介面方法
        /// </summary>
        public void Fly()
        {
            Console.WriteLine("我是一架飛機,我也能飛~~");
        }
    }
複製程式碼

在Main主函式中,介面IFlyable陣列,新增Plane物件:

複製程式碼
    class Program
    {
        static void Main(string[] args)
        {
            //建立一個IFlyable介面陣列,新增 Magpie物件,Eagle物件,Plane物件
            IFlyable[] flys = { 
                           new Magpie(),
                           new Eagle(),
                           new Plane()
            };
            //遍歷一下flys陣列
            foreach (IFlyable fly in flys)
            {
                fly.Fly();
            }
            Console.ReadKey();
        }
    }
複製程式碼

執行結果:

由此,可以看出用介面實現多型程式的擴充套件性得到了大大提升,以後不管是再擴充套件一個蝴蝶(Butterfly),還是鳥人(Birder)建立一個類,實現這個介面,在主函式中新增該物件就可以了。
也不需要檢視原始碼是如何實現的,體現了開放封閉原則!

介面充分體現了多型的魅力~~

 

以上通過一些小的事例,給大家介紹了面向物件中三種實現多型的方式,或許有人會問,在專案中怎麼使用多型呢?多型的魅力在專案中如何體現?
那麼接下來我做一個面向物件的簡單計算器,來Show一下多型在專案中使用吧!

加減乘除運算,我們可以根據共性提取出一個計算類,裡面包含兩個屬性 Number1和Number2,還有一個抽象方法Compute();程式碼如下:

複製程式碼
    /// <summary>
    /// 計算父類
    /// </summary>
    public abstract class Calculate
    {
        public int Number1
        {
            get;
            set;
        }
        public int Number2
        {
            get;
            set;
        }
        public abstract int Compute();
    }
複製程式碼

接下來,我們新增一個加法器,繼承計算Calculate父類:

複製程式碼
    /// <summary>
    /// 加法器
    /// </summary>
    public class Addition : Calculate
    {
        /// <summary>
        /// 實現父類計算方法
        /// </summary>
        /// <returns>加法計算結果</returns>
        public override int Compute()
        {
            return Number1 + Number2;
        }
    }
複製程式碼

再新增一個減法器,繼承計算Calculate父類:

複製程式碼
    /// <summary>
    /// 減法器
    /// </summary>
    public class Subtraction : Calculate
    {
        /// <summary>
        /// 實現父類計算方法
        /// </summary>
        /// <returns>減法計算結果</returns>
        public override int Compute()
        {
            return Number1 - Number2;
        }
    }
複製程式碼

在主窗體FormMain中,編寫計算事件btn_Compute_Click,程式碼如下:

複製程式碼
    private void btn_Compute_Click(object sender, EventArgs e)
    {
        //獲取兩個引數
        int number1 = Convert.ToInt32(this.txt_Number1.Text.Trim());
        int number2 = Convert.ToInt32(this.txt_Number2.Text.Trim());
        //獲取運算子
        string operation = cbb_Operator.Text.Trim();
        //通過運算子,返回父類型別
        Calculate calculate = GetCalculateResult(operation);
        calculate.Number1 = number1;
        calculate.Number2 = number2;
        //利用多型,返回運算結果
        string result = calculate.Compute().ToString();
        this.lab_Result.Text = result;
    }
    /// <summary>
    /// 通過運算子,返回父類型別
    /// </summary>
    /// <param name="operation"></param>
    /// <returns></returns>
    private Calculate GetCalculateResult(string operation)
    {
        Calculate calculate = null;
        switch (operation)
        {
            case "+":
                calculate = new Addition();
                break;
            case "-":
                calculate = new Subtraction();
                break;
        }
        return calculate;
    }
複製程式碼

在該事件中主要呼叫GetCalculateResult方法,通過運算子,建立一個對應的加減乘除計算器子類,然後賦值給父類,其實這就是設計模式中的簡單工廠設計模式,我給你一個運算子你給我生產一個對應的加減乘除計算器子類,返回給我。。其實大多數的設計模式的核心就是多型,掌握好多型,設計模式看起來也很輕鬆。

現階段工作已經完成,但是過了一段時間,又新增新的需求了,我還要擴充套件一個乘法了,那好,很簡單隻要建立一個乘法計算器繼承Calculate父類即可,看程式碼:

複製程式碼
    /// <summary>
    /// 乘法計算器
    /// </summary>
    public  class Multiplication:Calculate
    {
        public override int Compute()
        {
            return Number1*Number2;
        }
    }
複製程式碼

然後在GetCalculateResult函式中新增一個case 就好了:

複製程式碼
    switch (operation)
    {
        case "+":
            calculate = new Addition();
            break;
        case "-":
            calculate = new Subtraction();
            break;
        case "*":
            calculate = new Multiplication();
            break;
    }
複製程式碼

執行結果:

好了,就這麼方便,一個新的功能就擴充套件完畢了,我根本不需要檢視原始碼是如何實現的,這就是多型的好處!