扯一扯 C#委託和事件?策略模式?接口回調?

早前學習委託的時候,寫過一點東西,今天帶着新的思考和認知,再記點東西。這篇文章扯到設計模式中的策略模式,觀察者模式,還有.NET的特性之一——委託。真的,請相信我,我只是在扯淡......html

場景練習

還記得這兩我的嗎?李雷和韓梅梅,他們見面在打招呼...假設李雷和韓梅梅在中文課上打招呼和英文可上打招呼方式不同。下面定義兩個方法來表示打招呼:java

//中文方式打招呼
    public void ChineseGreeting(string name){
        Console.WriteLine("吃飯了嗎?"+name);
    }

    //英文方式打招呼
    public void EnglishGreeting(string name){
        Console.WriteLine("How are you,"+name);
    }

如今,咱們選擇哪一種打招呼的方式,由上中文課仍是英文課來決定。再定義一個變量 lesson 表示上的課程,則打招呼方法以下:算法

public void GreetPeople(string name){
        switch(lesson){
            case Chinese:
                ChineseGreeting(name);
                break;
            case English:
                EnglishGreeting(name);
                break;
            default:
                break;
        }
    }

剛入行的coder也很容易想到這樣的代碼來模擬該場景。上中文課,則lesson=Chinese ,若是上英文課,則 lesson=English ,你可能還會想到定義一個枚舉去表示課程類型。很簡單,完整的程序以及Test代碼就不給出了。如今問題來了,假設之後又多了日文課,韓文課,俄語課,咱們還須要在該類中不斷增長新的語種打招呼的方式,還須要增長枚舉的取值(若是你使用了枚舉定義課程的話),而且每增長一種,GreetPeople 方法還須要不斷修改,這個類會愈來愈臃腫。顯然,這個類違反了單一職責的原則,顯得很混亂,不利於維護。
爲了解決這個問題,咱們有請策略模式登場:設計模式

策略模式

先上代碼:數據結構

public class English : IGreetPeople
    {
        public void IGreetPeople.GreetPeople(string name)
        {
            Console.WriteLine("How are you, " + name);
        }
    }

    public class Chinese : IGreetPeople
    {
        public void GreetPeople(string name)
        {
            Console.WriteLine("吃飯了嗎?" + name);
        }
    }

    public class English : IGreetPeople
    {
        void IGreetPeople.GreetPeople(string name)
        {
            Console.WriteLine("How are you, " + name);
        }
    }

    public class Greeting
    {
        private IGreetPeople mGreetPeopleImpl;

        public Greeting() { }
        
        public Greeting(IGreetPeople greetPeople)
        {
            this.mGreetPeopleImpl = greetPeople;
        }

        public void SetGreetPeople(IGreetPeople greetPeople)
        {
            this.mGreetPeopleImpl = greetPeople;
        }

        public void GreetPeople(string name)
        {
            mGreetPeopleImpl.GreetPeople(name);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Greeting greeting1 = new Greeting(new Chinese());
            Greeting greeting2 = new Greeting();
            greeting2.SetGreetPeople(new English());

            greeting1.GreetPeople("李雷");
            greeting2.GreetPeople("HanMeimei");

            Console.ReadKey();

        }
    }

運行結果以下:併發

吃了飯了嗎?李雷
    How are you,HanMeimei

這裏定義了一個打招呼的接口,不一樣的語種,分別實現這個接口。具體打招呼的方法,則調用打招呼的類 Greeting 裏面的 GreetPeople 方法。在 Greeting 類裏面將不一樣的語種對象複製給它的 mGreetPeopleImpl 屬性。這樣每一個類的功能單一,對於不一樣的語種打招呼方式,咱們只需在建立 Greeting 對象時給它的 mGreetPeopleImpl 屬性附上對應的值。less


策略模式的結構:

-抽象策略類(Strategy):定義全部支持的算法的公共接口。 Context使用這個接口來調用某ConcreteStrategy定義的算法。
-具體策略類(ConcreteStrategy):以Strategy接口實現某具體算法。
-環境類(Context):用一個ConcreteStrategy對象來配置。維護一個對Strategy對象的引用。可定義一個接口來讓Strategy訪問它的數據。學習

策略模式的適用場景

• 許多相關的類僅僅是行爲有異。 「策略」提供了一種用多個行爲中的一個行爲來配置一個類的方法。即一個系統須要動態地在幾種算法中選擇一種。
• 須要使用一個算法的不一樣變體。例如,你可能會定義一些反映不一樣的空間 /時間權衡的算法。當這些變體實現爲一個算法的類層次時 ,可使用策略模式。
• 算法使用客戶不該該知道的數據。可以使用策略模式以免暴露覆雜的、與算法相關的數據結構。
• 一個類定義了多種行爲 , 而且這些行爲在這個類的操做中以多個條件語句的形式出現。將相關的條件分支移入它們各自的Strategy類中以代替這些條件語句。測試

策略模式是一種設計模式,核心思想是面向對象的特性。java,C# 等面向對象的語言均可以利用這種思想有效解決上面打招呼的場景問題。接下來要說的是 .NET 平臺的一種特性——委託一樣能夠解決此類問題。爲了更好的理解委託,先來簡單講講接口回調。this

接口回調

所謂的接口回調,簡單講就是在一個類中去調用另外一個類的方法,在該方法中再回調調用者者自己的的方法。也許我表達的不夠準備,來個實際例子說明吧:
假設李雷要作算術運算,要求兩個數相除,李雷不會除法,它讓韓梅梅幫他計算,韓梅梅算出結果以後,再由李雷來處理結果。上代碼:

定義了一個接口

public interface ICalculateCallBack
    {
        void onSuccess(int result);
        void onFailed();
    }

    public class HanMeimei
    {

        private ICalculateCallBack callback;

        public HanMeimei(ICalculateCallBack callback)
        {
            this.callback = callback;
        }

        public void doWork(int num1, int num2)
        {
            if (num2 == 0)
            {
                callback.onFailed();
            }
            else
            {
                int result = num1 / num2;
                callback.onSuccess(result);
            }
        }
    }

     public class LiLei
    {

        HanMeimei mHanMeimei = new HanMeimei(new MyCalCallback());

        public void Calculate(int num1,int num2)
        {
            mHanMeimei.doWork(num1, num2);
        }

        class MyCalCallback : ICalculateCallBack
        {
            void ICalculateCallBack.onFailed()
            {
                Console.WriteLine("除數不能爲0");
            }

            void ICalculateCallBack.onSuccess(int result)
            {
                Console.WriteLine("結果是:" + result);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            new LiLei().Calculate(7, 2);
            Console.ReadLine();

            new LiLei().Calculate(3, 0);
            Console.ReadLine();
        }
    }

執行結果是

結果是:3

    除數不能爲0

首先,咱們定義了一個接口,用來處理計算成功和計算失敗(除數爲0)的狀況。李雷的 calculate 方法實際調用的是韓梅梅的 doWork 方法,將要計算的兩個數傳過去。而後,韓梅梅計算以後,在經過 ICalculatecallback 中的方法將結果返回給李雷。ICalculatecallback 中的方法是在它的實現類 MyCalCallback 中具體實現的(若是是java,能夠不用定義這個類採用實現接口的匿名內部類的方式)。這個過程實際能夠看作是回調李雷的方法。由於是李雷纔是決定拿到計算結果乾什麼(這裏只是純粹地在控制檯打印出來)的主角。這就是接口回調的整個過程,也很簡單。
看到這裏,你可能會問,這不是畫蛇添足嗎?徹底能夠不定義這個 ICalculateCallback 接口,在李雷的類中定義一個 int 型的變量去接收調用韓梅梅的方法以後的返回值,而後拿着這個返回值,想幹什麼就幹什麼。沒錯,這個例子中的確能夠這麼作。試想一下,假設韓梅梅去計算結果的這個過程比較耗時,通常這種狀況,咱們會開一個新線程去執行。因爲併發過程當中,咱們並不知道韓梅梅何時會計算出結果,這時候接直接調用的方式就存在問題了,極可能你拿這計算結果去作處理的時候,發現計算尚未完成,而接口回調就能很好地解決這個問題。
好吧,這個場景和第一個場景並無什麼關係,扯得有點遠了,可是請放下磚頭,由於我一開始就聲明瞭,我只是在扯淡。

委託

委託是 .NET 平臺的一種特性,能夠好不誇張的說,不學會委託,等於不會 .NET。兩年前我還有一部分工做須要用 C# 完成的時候,看過一些 C# 的書籍,不少入門的書籍都根本不會介紹委託和事件。看過一些博客什麼的,當時感受本身學懂了委託和事件。兩年過去了,雖然這兩年沒再繼續使用 C# , 可是兩年時間的積累,對面向對象思想理解顯然比兩年前更上了一個層次。再回過頭來看委託和事件,我以爲當時的理解可能真的很淺(也許再過兩年,又會以爲如今的理解很淺)。因此下面所說的委託只是基於我如今的理解,通俗點講,就是又在扯淡。
回到第一個打招呼的場景,前面說不用策略模式也能夠解決。解決問題的根本思想是咱們針對不一樣的語種,調用了不一樣的打招呼方式。試想,若是咱們可以把方法名做爲參數傳遞給另外一個方法,雖然這聽起來有點繞口,但這樣咱們就知道該調用什麼方法了。

//中文方式打招呼
    public void ChineseGreeting(string name){
        Console.WriteLine("吃飯了嗎?"+name);
    }

    //英文方式打招呼
    public void EnglishGreeting(string name){
        Console.WriteLine("How are you,"+name);
    }

把這兩個方法搬下來,下面假設有這樣一個方法:

public void GreetPeople(String name, ???  XXXGreeting){
        XXXGreeting(name);
    }

這裏 ??? 表示某種類型,形參 XXXGreeting 即表明要傳入的 ChineseGreeting 或者 EnglishGreeting,參數傳什麼,實際就調用什麼。這個不難理解,就是我前面說的把方法名做爲參數傳遞進去,這樣當是中文課的時候,我只須要調用

GreetPeople("李雷",ChineseGreeting);

當是英文課的時候,我就調用

GreetPeople("HanMeimei",EnglishGreeting);

代碼很簡潔,避免了每次去修改 if-else 或者 switch-case 。那麼問題來了,這個 ??? 究竟是個什麼類型。能看我扯到如今的人都能猜到——沒錯,就是委託。
能夠把委託當作一種類型來看待。爲了方便說明,仍是先上代碼。完整代碼以下:

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

namespace StrategyTest
{

    class Program
    {
        delegate void GreetingDelegate(String name);

        static void Main(string[] args)
        {
            GreetPeople("李雷", new Program().ChineseGreeting);
            GreetPeople("HanMeimei", new Program().EnglishGreeting);
            Console.ReadLine();
        }


        static void GreetPeople(String name, GreetingDelegate XXXGreeting)
        {
            XXXGreeting(name);
        }


        public void ChineseGreeting(String name)
        {
            Console.WriteLine("吃飯了嗎?" + name);
        }

        public void EnglishGreeting(string name)
        {
            Console.WriteLine("How are you," + name);
        }

    }
}

運行結果和前面同樣,

吃飯了嗎?李雷
    How are you,HanMeimei

代碼開始,利用 delegate 關鍵字,聲明瞭委託類型:
delegate void GreetingDelegate(String name);
委託的聲明,與方法的聲明相似,帶有任意個數的參數,而且有返回值,固然前面還能夠加上相應的權限修飾符。須要注意的是,聲明委託時的參數和返回值與咱們須要傳入和使用的方法參數個數和類型以及返回值保持一致。好比這個例子中中文打招呼的方法和英文打招呼的方法,都接收一個字符串類型的參數,而且都沒有返回值,那麼咱們聲明委託的時候,也須要接收一個字符串類型的參數,而且沒有返回值。
從面相對象的角度來說,既然咱們把委託當作一種類型來看,那麼咱們能夠直接使用委託的實例對象去調用方法。
咱們修改代碼:

class Program
    {
        delegate void GreetingDelegate(String name);

        static void Main(string[] args)
        {
            //GreetPeople("李雷", new Program().ChineseGreeting);
            //GreetPeople("HanMeimei", new Program().EnglishGreeting);

            GreetingDelegate greetingDelegate = new GreetingDelegate(new Program().ChineseGreeting);
            greetingDelegate("李雷");

            Console.ReadLine();
        }

        //static void GreetPeople(String name, GreetingDelegate XXXGreeting)
        //{
        //    XXXGreeting(name);
        //}

        public void ChineseGreeting(String name)
        {
            Console.WriteLine("吃飯了嗎?" + name);
        }

        public void EnglishGreeting(string name)
        {
            Console.WriteLine("How are you," + name);
        }

    }

執行結果:

吃飯了嗎?李雷

委託表明了一類方法,這裏咱們就相似初始化一個類的對象同樣,聲明瞭委託 GreetingDelegate 的一個對象 greetingDelegate ,同時,給它綁定了一個方法 ChinsesGreeting 。給委託綁定方法,這個術語,就是委託最核心的內容。而且,咱們能夠給一個委託同時綁定多個方法,調用的時候,會一次執行這些方法。給委託綁定方法使用 += 符號。再次修改代碼:

class Program
    {
        delegate void GreetingDelegate(String name);

        static void Main(string[] args)
        {
            //GreetPeople("李雷", new Program().ChineseGreeting);
            //GreetPeople("HanMeimei", new Program().EnglishGreeting);

            GreetingDelegate greetingDelegate = new GreetingDelegate(new Program().ChineseGreeting);
            greetingDelegate += new Program().EnglishGreeting;
            greetingDelegate("李雷");
            Console.ReadLine();
        }

        //static void GreetPeople(String name, GreetingDelegate XXXGreeting)
        //{
        //    XXXGreeting(name);
        //}

        public void ChineseGreeting(String name)
        {
            Console.WriteLine("吃飯了嗎?" + name);
        }

        public void EnglishGreeting(string name)
        {
            Console.WriteLine("How are you," + name);
        }
    }

此次執行的結果是:

吃飯了嗎?李雷
    How are you,李雷

能夠給委託綁定方法,固然也能夠給委託解除綁定方法,用 -= 符號。咱們再加一行代碼,把中文打招呼的方式給取消掉:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);
            
    GreetingDelegate greetingDelegate = new GreetingDelegate(new Program().ChineseGreeting);
    greetingDelegate += new Program().EnglishGreeting;
    greetingDelegate -= new Program().ChineseGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

執行結果是:

吃飯了嗎?李雷
    How are you,李雷

What's the F**K?
結果爲何沒有變化,不是應該只有英文打招呼的方式嗎?別急。多看一眼就會發現,咱們經過 -= 取消綁定的方法,並非以前初始化時綁定上的那個方法,咱們這裏分別 new 了三個對象。因此咱們後來取消綁定的是一個新對象的方法,而這個方法根本就沒有綁定過。再稍微修改一下:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);

    Program p = new Program();
    GreetingDelegate greetingDelegate = new GreetingDelegate(p.ChineseGreeting);
    greetingDelegate += p.EnglishGreeting;
    greetingDelegate -= p.ChineseGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

此次的執行結果就對了:

How are you,李雷

這就是委託最基本的應用了,委託能夠實現接口回調同樣的效果,被調用的方法能夠回調調用者的方法,具體的方法體實現由調用者本身實現。下面可能會想到這樣作,聲明委託的時候,不綁定方法,而後所有經過 += 來綁定方法,這樣看起來,彷佛更協調。理想中的代碼應該想下面這樣:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);

    Program p = new Program();
    GreetingDelegate greetingDelegate = new GreetingDelegate();
    greetingDelegate += p.ChineseGreeting;
    greetingDelegate += p.EnglishGreeting;
    greetingDelegate -= p.ChineseGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

很遺憾,這樣的代碼不能經過編譯,報錯:GreetingDelegate不包含採用0個參數的構造方法。 而下面的代碼是能夠的:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);

    Program p = new Program();
    GreetingDelegate greetingDelegate;
    greetingDelegate = p.ChineseGreeting;
    greetingDelegate += p.EnglishGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

面向對象三大特性之一就是封裝性,該私有的私有,該公開的公開。 好比咱們自定義一個類的時候,通常字段採用 private 修飾,不讓外面引用該字段,外界須要操做該字段,咱們會根據須要添加相應公開的 get 和 set 方法。顯然上面這段代碼違背了對象的封裝性。爲了解決這個問題,C# 中引入了一個新的關鍵字 event ——對應概念,事件。

事件

能夠說事件是對委託的一種拓展。事件實現了對委託對象的封裝。說明事件以前,先來了解下 .Net Framework 的關於委託和事件的編碼規範要求:
委託類型的名稱都應該以EventHandler結束。
委託的原型定義:有一個void返回值,並接受兩個輸入參數:一個Object 類型,一個 EventArgs類型(或繼承自EventArgs)。
事件的命名爲 委託去掉 EventHandler以後剩餘的部分。
繼承自EventArgs的類型應該以EventArgs結尾。

爲了更好地說明這個事件的做用,而且以要求的規範命名,下面換一個例子來講明問題,以.NET的事件驅動模型來講明問題吧,如今要作的事是,點擊一個按鈕,而後打印出相關的信息。
首先,定義一個 MyButton 的類。代碼以下:該類

namespace EventTest
{
    //定義一個委託類型,用來調用按鈕的點擊事件
    public delegate void ClickEventHandler(Object sender,MyEventArgs e);

    //定義MyButton類
    public class MyButton
    {
        //定義委託實例
        public ClickEventHandler Click;

        //定義按鍵的信息
        private string msg;

        public MyButton(string msg)
        {
            this.msg = msg;
        }

        //按鍵的點擊方法
        public void OnClick()
        {
            if (Click != null)
            {
                Click(this,new MyEventArgs(msg));
            }
        }
    }
}

再看,MyEventArgs 是什麼:

namespace EventTest
{
    public class MyEventArgs:EventArgs
    {
        private string msg;

        public MyEventArgs(string msg)
        {
            this.msg = msg;
        }

        public string getMsg()
        {
            return this.msg;
        }
        
        public void setMsg(string msg)
        {
            this.msg = msg;
        }

    }
}

MyEventArgs繼承自 EventArgs。接下來再看,咱們的主程序:

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MyButton mb = new MyButton("A");
            mb.Click = new ClickEventHandler(Button_Click);
            mb.OnClick();
            mb.Click(mb, new MyEventArgs("封裝性很差"));
            Console.ReadLine();
        }

        private static void Button_Click(Object sender, MyEventArgs e)
        {
            Console.WriteLine(e.getMsg());
        }
    }
}

輸出結果以下:

A
    封裝性很差

個人本意是好比,mb 這個對象表明 A 鍵,執行 OnClick 方法,打印出 A,這裏咱們看到了,下面竟然能夠繞過 MyButton 的 OnClick 方法,直接執行了委託所綁定的方法,咱們能夠隨意更改這個委託。
下面將 event 這個關鍵字,在聲明委託實例以前加上。

namespace EventTest
{
    public delegate void ClickEventHandler(Object sender,MyEventArgs e);

    public class MyButton
    {
        public event ClickEventHandler Click;
        private string msg;

        public MyButton(string msg)
        {
            this.msg = msg;
        }

        public void OnClick()
        {
            if (Click != null)

            {
                Click(this,new MyEventArgs(msg));
            }
        }
    }
}

而後代碼立馬報錯了,錯誤信息以下:


這個錯誤信息很明確,事件只能給它綁定方法和接觸綁定方法,不能直接賦值,也不可以再直接調用了。改正錯誤後的代碼以下:

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MyButton mb = new MyButton("A");
            mb.Click += Button_Click;
            mb.OnClick();
            Console.ReadLine();
        }

        private static void Button_Click(Object sender, MyEventArgs e)
        {
            Console.WriteLine(e.getMsg());
        }
    }
}

這時候運行結果只有 A

到這裏,event 的做用也就一目瞭然了。你那麼還有最後一個疑問,定義委託的時候,這兩個參數的做用,其中第二個參數 EventArgs 已經用到,能夠體會一下。第一個參數 Object 是什麼,有什麼做用。其實這個例子中徹底能夠只用一個string類型的參數,這麼寫是爲了按照委託的原型寫法來寫。爲了進一步說明問題,咱們在 MyButton 類中增長一個方法,並修改主程序代碼:

namespace EventTest
{
    public delegate void ClickEventHandler(Object sender,MyEventArgs e);

    public class MyButton
    {
        public event ClickEventHandler Click;
        private string msg;

        public MyButton(string msg)
        {
            this.msg = msg;
        }

        public void OnClick()
        {
            if (Click != null)
            {
                Click(this,new MyEventArgs(msg));
            }
        }

        public void print()
        {
            Console.WriteLine("這是一個測試!");
        }
    }
}

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MyButton mb = new MyButton("A");
            mb.Click += Button_Click;
            mb.OnClick();
            Console.ReadLine();
        }

        private static void Button_Click(Object sender, MyEventArgs e)
        {
            ((MyButton)sender).print();
            Console.WriteLine(e.getMsg());
        }
    }
}

結果不言而喻。本例中這兩個參數,委託聲明原型中的Object類型的參數表明了MyButton對象,主程序能夠經過它訪問觸發事件的對象(Heater)。
EventArgs 對象包含了主程序所感興趣的數據,在本例中是MyButton的字段 msg。若是你熟悉觀察者模式的推拉模型,那麼這兩個參數,正好跟觀察者模式推拉兩種模型須要傳遞的參數所表達的意義吻合。這也說明了利用委託能夠很容易實現觀察者模式。
若是你還不熟悉觀察者模式,能夠看下我另外一篇文章裏扯的內容:和觀察者模式來一次約談

相關文章
相關標籤/搜索