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


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

場景練習

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

    //中文方式打招呼
    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 屬性附上對應的值。


策略模式的結構:

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

策略模式的適用場景

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

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

接口回調

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

定義了一個接口

    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。如果你熟悉觀察者模式的推拉模型,那么這兩個參數,正好跟觀察者模式推拉兩種模型需要傳遞的參數所表達的意義吻合。這也說明了利用委托可以很容易實現觀察者模式。
如果你還不熟悉觀察者模式,可以看下我另一篇文章里扯的內容:和觀察者模式來一次約談


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM