第12章 委託與事件

委託和事件在 .NET Framework 中的應用很是普遍,然而,較好地理解委託和事件對不少接觸 C# 時間不長的人來講並不容易。它們就像是一道檻兒,過了這個檻的人,以爲真是太容易了,而沒有過去的人每次見到委託和事件就以爲內心堵得慌,渾身不自在。本章中,我將由淺入深地講述什麼是委託、爲何要使用委託、事件的由來、.NET Framework 中的委託和事件、委託中方法異常和超時的處理、委託與異步編程、委託和事件對Observer 設計模式的意義,對它們的編譯代碼也作了討論。編程

12.1將方法做爲方法的參數 設計模式

咱們先無論這個標題如何的繞口,也無論委託到底是個什麼東西,來看下面這兩個最簡單的方法,它們不過是在屏幕上輸出一句問候的話語:異步

示例1 異步編程

class Program函數

{ 工具

static void Main(string[] args) 測試

{ this

Program p = new Program(); spa

p.GreetPeople("Jimmy"); 翻譯

}

public void GreetPeople(string name)

{

EnglishGreeting(name);

}

public void EnglishGreeting(string name)

{

Console.WriteLine("Morning, " + name);

}

}

暫且無論這兩個方法有沒有什麼實際意義。GreetPeople用於向某人問好,當咱們傳遞表明某人姓名的name參數,好比說"Jimmy",進去的時候,在這個方法中,將調用EnglishGreeting方法,再次傳遞name參數,EnglishGreeting則用於向屏幕輸出 "Morning, Jimmy"。如圖12-1所示。

12-1 歡迎語顯示結果

如今假設這個程序須要進行全球化,哎呀,很差了,我是中國人,我不明白"Good Morning"是什麼意思,怎麼辦呢?好吧,咱們再加個中文版的問候方法:

public void ChineseGreeting(string name)

{

Console.WriteLine("早上好, " + name);

}

這時候,GreetPeople 也須要改一改了,否則如何判斷到底用哪一個版本的 Greeting 問候方法合適呢?在進行這個以前,咱們最好再定義一個枚舉做爲判斷的依據:

示例2

public enum Language

{

English, Chinese

}

public void GreetPeople(string name, Language lang)

{

switch(lang)

{

case Language.English:

EnglishGreeting(name);

break;

case Language.Chinese:

ChineseGreeting(name);

break;

}

}

修改好以後在程序入口函數中調用被重載的新方法,

public void GreetPeople(string name, Language lang)

總體代碼如示例3所示:

示例3

class Program

{

static void Main(string[] args)

{

Program p = new Program();

p.GreetPeople("傑米",Language.Chinese);

}

public void GreetPeople(string name)

{

EnglishGreeting(name);

}

public void ChineseGreeting(string name)

{

Console.WriteLine("早上好, " + name);

}

public void EnglishGreeting(string name)

{

Console.WriteLine("Morning, " + name);

}

public enum Language

{

English, Chinese

}

public void GreetPeople(string name, Language lang){

switch(lang){

case Language.English:

EnglishGreeting(name);

break;

case Language.Chinese:

ChineseGreeting(name);

break;

}

}

}

結果如圖12-2所示。

 

 

12-2 歡迎語顯示結果

儘管這樣解決了問題,但你們也很容易想到,這個解決方案的可擴展性不好,若是往後咱們須要再添加韓文版、日文版,就不得不反覆修改枚舉和GreetPeople()方法,以適應新的需求。

在考慮新的解決方案以前,咱們先看看 GreetPeople的方法簽名(方法簽名包括:1.返回類型,2.參數列表,3方法名稱):

public void GreetPeople(string name, Language lang)

咱們僅看 string name,在這裏,string 是參數類型,name 是參數變量,當咱們賦給name字符串"jimmy"時,它就表明"jimmy"這個值;當咱們賦給它"張子陽"時,它又表明着"張子陽"這個值。而後,咱們能夠在方法體內對這個name進行其餘操做。

若是你再仔細想一想,假如GreetPeople()方法能夠接受一個參數變量,這個變量能夠表明另外一個方法,當咱們給這個變量賦值 EnglishGreeting的時候,它表明着 EnglsihGreeting() 這個方法;當咱們給它賦值ChineseGreeting 的時候,它又表明着ChineseGreeting()方法。咱們將這個參數變量命名爲 MakeGreeting,那麼不就能夠給name賦值時同樣,在調用 GreetPeople()方法的時候,給這個MakeGreeting 參數也賦上值嗎(ChineseGreeting或者EnglsihGreeting)?而後,咱們在方法體內,也能夠像使用別的參數同樣使用MakeGreeting。可是,因爲MakeGreeting表明着一個方法,它的使用方式應該和它被賦的方法(好比ChineseGreeting)是同樣的,好比:MakeGreeting(name);好了,有了思路了,咱們如今就來改改GreetPeople()方法,那麼它應該是這個樣子了:

public void GreetPeople(string name, *** MakeGreeting)

{

MakeGreeting(name);

}

注意到 *** ,這個位置一般放置的應該是參數的類型,但到目前爲止,咱們僅僅是想到應該有個能夠表明方法的參數,並按這個思路去改寫GreetPeople方法,如今就出現了一個大問題:這個表明着方法的MakeGreeting參數應該是什麼類型的?

注意:這裏已再也不須要枚舉了,由於在給MakeGreeting賦值的時候動態地決定使用哪一個方法,是ChineseGreeting仍是 EnglishGreeting,而在這兩個方法內部,已經對使用"morning"仍是"早上好"做了區分。

聰明的你應該已經想到了,如今是委託該出場的時候了,但講述委託以前,咱們再看看MakeGreeting參數所能表明的 ChineseGreeting()EnglishGreeting()方法的簽名:

public void EnglishGreeting(string name)

public void ChineseGreeting(string name)

如同name能夠接受String類型的"true""1",但不能接受bool類型的trueint類型的1同樣。MakeGreeting的參數類型定義應該可以肯定 MakeGreeting能夠表明的方法種類,再進一步講,就是MakeGreeting能夠表明的方法的參數類型和返回類型。

因而,委託出現了:它定義了MakeGreeting參數所能表明的方法的種類,也就是MakeGreeting參數的類型。

注意:若是上面這句話比較繞口,咱們就把它翻譯成這樣:string 定義了name參數所能表明的值的種類,也就是name參數的類型。

委託的定義:

public delegate void GreetingDelegate(string name);

能夠與上面EnglishGreeting()方法的簽名對比一下,除了加入delegate關鍵字之外,其他的是否是徹底同樣?

如今,讓咱們再次改動GreetPeople()方法,如示例4所示:

示例4

public delegate void GreetingDelegate(string name);

public void GreetPeople(string name, GreetingDelegate makeGreeting){

makeGreeting(name);

}

如你所見,委託GreetingDelegate出現的位置與 string相同,string是一個類型,那麼GreetingDelegate應該也是一個類型,或者叫類(Class)。可是委託的聲明方式和類卻徹底不一樣,這是怎麼一回事?實際上,委託在編譯的時候確實會編譯成類。由於Delegate是一個類,因此在任何能夠聲明類的地方均可以聲明委託。更多的內容將在下面講述,如今,請看看這個範例的完整代碼:

示例5

//定義委託,它定義了能夠表明的方法的類型

public delegate void GreetingDelegate(string name);

   

class Program

{

private static void EnglishGreeting(string name)

{

Console.WriteLine("Morning, " +name);

}

private static void ChineseGreeting(string name)

{

Console.WriteLine("早上好, " +name);

}

//注意此方法,它接受一個GreetingDelegate類型的方法做爲參數

private static void GreetPeople(string name,

GreetingDelegate MakeGreeting)

{

MakeGreeting(name);

}

static void Main(string[] args)

{

GreetPeople("Jimmy Zhang",EnglishGreeting);

GreetPeople("張子陽",ChineseGreeting);

}

}

運行結構如圖12-3所示。

 

 

12-3 示例5運行結果

咱們如今對委託作一個總結: 委託是一個類,它定義了方法的類型,使得能夠將方法看成另外一個方法的參數來進行傳遞,這種將方法動態地賦給參數的作法,能夠避免在程序中大量使用if-else(switch)語句,同時使得程序具備更好的可擴展性。

將方法綁定到委託,到這裏,是否是有那麼點如夢初醒的感受?因而,你是否是在想:在上面的例子中,我不必定要直接在GreetPeople()方法中給 name參數賦值,我能夠像這樣使用變量:

示例6

static void Main(string[] args)

{

string name1, name2; //聲明

name1 = "Jimmy Funy"; //賦值

name2 = "王小毛";

GreetPeople(name1, EnglishGreeting);

GreetPeople(name2, ChineseGreeting);

}

而既然委託GreetingDelegate 和類型 string 的地位同樣,都是定義了一種參數類型,那麼,咱們是否是也能夠這麼使用委託?答案是確定的,如實例7所示。

示例7

GreetingDelegate delegate1, delegate2; //聲明

delegate1 = EnglishGreeting; //賦值

delegate2 = ChineseGreeting;

   

GreetPeople("Jimmy Funy", delegate1);

GreetPeople("王小毛", delegate2);

委託還有不一樣於string的一個特性:能夠將多個方法賦給同一個委託,或者叫將多個方法綁定到同一個委託。當調用這個委託的時候,將依次調用其所綁定的方法。語法以下:

示例8

static void Main(string[] args)

{

GreetingDelegate delegate1;

delegate1 = EnglishGreeting;

delegate1 += ChineseGreeting; //綁定第二個方法

   

GreetPeople("Jimmy Funy", delegate1);

Console.ReadKey();

}

運行結果如圖12-4所示。

 

 

12-4 委託綁定多個方法

實際上,咱們還能夠跳過GreetPeople方法,經過委託來直接調用EnglishGreetingChineseGreeting而且與之傳參。如示例9所示。

示例9

static void Main(string[] args)

{

GreetingDelegate delegate1;

delegate1 = EnglishGreeting; // 先給委託類型的變量賦值

delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法

// 將前後調用 EnglishGreeting ChineseGreeting方法

delegate1("Jimmy Funy");

Console.ReadKey();

}

運行結果同圖12-4

注意:

再示例9中,第一次用的"=",是賦值的語法;第二次,用的是"+=",是綁定的語法。若是第一次就使用"+=",將出現"使用了未賦值的局部變量"的編譯錯誤。能夠經過下面語句給委託初始化。

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);

既然給委託能夠綁定一個方法,那麼也應該有辦法取消對方法的綁定,很容易想到,這個語法是"-="。如示例10所示。

示例10

static void Main(string[] args)

{

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);

delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法

   

// 將前後調用 EnglishGreeting ChineseGreeting 方法

GreetPeople("Jimmy Funy", delegate1);

Console.WriteLine();

   

delegate1 -= EnglishGreeting; //取消對EnglishGreeting方法的綁定

// 將僅調用 ChineseGreeting

GreetPeople("王小毛", delegate1);

Console.ReadKey();

}

12.2 事件

12.2.1 事件的由來

咱們繼續思考上面的程序:上面的三個方法都定義在Programe類中,這樣作是爲了理解的方便,實際應用中,一般都是 GreetPeople 在一個類中,ChineseGreeting EnglishGreeting 在另外的類中。如今你已經對委託有了初步瞭解,應該對上面的例子作一些改進了。假設咱們將GreetingPeople()放在一個叫GreetingManager的類中,那麼新程序應該是這個樣子的?

示例11

namespace Delegate

{

//定義委託,它定義了能夠表明的方法的類型

public delegate void GreetingDelegate(string name);

//新建的GreetingManager

public class GreetingManager

{

public void GreetPeople(string name, GreetingDelegate MakeGreeting)

{

MakeGreeting(name);

}

}

class Program

{

private static void EnglishGreeting(string name)

{

Console.WriteLine("Morning, " + name);

}

private static void ChineseGreeting(string name)

{

Console.WriteLine("早上好, " + name);

}

static void Main(string[] args)

{

GreetingManager gm = new GreetingManager();

GreetingDelegate delegate1;

//綁定多個方法

delegate1 = EnglishGreeting;

delegate1 += ChineseGreeting;

//調用方法

gm.GreetPeaple("Jimmy Funy", delegate1);

}

}

}

運行結果如圖12-4所示。

到了這裏,咱們不由想到:面向對象設計,講究的是對象的封裝,既然能夠聲明委託類型的變量(在上例中是delegate1),咱們何不將這個變量封裝到 GreetManager類中?在這個類的客戶端中使用不是更方便麼?因而,咱們改寫GreetManager類,像這樣:

示例12

//新建的GreetingManager

class GreetingManager

{

//GreetingManager類的內部聲明delegate1變量

public GreetingDelegate delegate1;

public void GreetingPeaple(string name)

{

//若是有方法註冊委託變量

if (delegate1 != null)

{

delegate1(name); //經過委託調用方法

}

}

}

//測試類Main()方法

static void Main(string[] args)

{

GreetingManager gm = new GreetingManager();

gm.delegate1 = EnglishGreeting;

gm.delegate1 += ChineseGreeting;

   

gm.GreetPeople("Jimmy Zhang");

}

儘管這樣達到了咱們要的效果,可是仍是存在着問題:

  • GreetingManager類中,delegate1成員被聲明爲public,這意味着調用者能夠對它進行隨意的賦值等操做,嚴重破壞對象的封裝性。但若是聲明爲private,客戶端對它根本就不可見,那它還有什麼用?
  • Main()方法中,第一個方法註冊用"=",是賦值語法,由於要進行實例化,第二個方法註冊則用的是"+="。可是,無論是賦值仍是註冊,都是將方法綁定到委託上,除了調用時前後順序不一樣,再沒有任何的分別,這樣讓人以爲很彆扭。
  • 對上面兩個問題,咱們天然會想到用對 delegate1進行封裝的方式來解決。那麼如何對委託進行封裝呢?答案是使用事件。

    12.2.2 什麼是事件

    事件是對委託的封裝。聲明事件的語法以下:

    訪問修飾符 event 委託類型 事件名;

    咱們改寫GreetingManager類,它變成了這個樣子:

    示例13

    public class GreetingManager

    {

    //聲明事件

    public event GreetingDelegate MakeGreet;

    public void GreetPeople(string name)

    {

    MakeGreet(name);

    }

    }

    咱們發現,MakeGreet 事件的聲明與以前委託變量 delegate1 的聲明惟一的區別是多了一個 event 關鍵字。那它是如何對委託進行封裝的呢?咱們藉助 Reflactor 工具來對 event 的聲明語句進行反編譯,獲得如下代碼:

    private GreetingDelegate MakeGreet;

    [MethodImpl(MethodImplOptions.Synchronized)]

    public void add_MakeGreet(GreetingDelegate value)

    {

    this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);

    }

    [MethodImpl(MethodImplOptions.Synchronized)]

    public void remove_MakeGreet(GreetingDelegate value)

    {

    this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);

    }

    經過反編譯代碼能明確:MakeGreet 事件確實是一個GreetingDelegate 類型的委託,只不過無論是否是聲明爲public,它老是被聲明爲private。另外,它還有兩個方法,分別是add_MakeGreetremove_MakeGreet,這兩個方法分別用於註冊委託類型的方法和取消註冊。實際上也就是:"+= "對應 add_MakeGreet"-="對應remove_MakeGreet。而這兩個方法的訪問限制取決於聲明事件時的訪問限制符。

    12.2.2 事件限制類型的能力

    使用事件不只能得到比委託更好的封裝性之外,還能限制含有事件的類型的能力。這是什麼意思呢?它的意思是說:事件應該由事件發佈者觸發,而不該該由事件的客戶端(客戶程序)來觸發。請看示例14

    示例14

    // 定義委託

    public delegate void NumberChangedEventHandler(int count);

    // 事件發佈者類

    public class Publishser

    {

    private int count;

    // 聲明委託變量

    public NumberChangedEventHandler NumberChanged;

    // 聲明一個事件

    //public event NumberChangedEventHandler NumberChanged;

       

    public void DoSomething()

    {

    // 在這裏完成一些工做 ...

    if (NumberChanged != null) // 觸發事件

    {

    count++;

    NumberChanged(count);

    }

    }

    }

    // 事件訂閱者類

    public class Subscriber

    {

    public void OnNumberChanged(int count)

    {

    Console.WriteLine("Subscriber notified: count = {0}", count);

    }

    }

    //測試類Main()方法

    static void Main(string[] args)

    {

    Publishser pub = new Publishser(); //發佈者對象

    Subscriber sub = new Subscriber(); //訂閱者對象

    pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);

    pub.DoSomething(); // 應該經過DoSomething()來觸發事件

    pub.NumberChanged(100); // 但能夠被這樣直接調用,對委託變量的不恰當使用

    }

    上面代碼定義了一個NumberChangedEventHandler 委託,而後咱們建立了事件的發佈者Publisher 和訂閱者Subscriber。當使用委託變量時,客戶端能夠直接經過委託變量觸發事件,也就是直接調用pub.NumberChanged(100),這將會影響到全部註冊了該委託的訂閱者。而事件的本意應該爲在事件發佈者在其自己的某個行爲中觸發,好比說在方法DoSomething()中知足某個條件後觸發。經過添加event 關鍵字來發布事件,事件發佈者的封裝性會更好,事件僅僅是供其餘類型訂閱,而客戶端不能直接觸發事件(語句pub.NumberChanged(100)沒法經過編譯),事件只能在事件發佈者Publisher 類的內部觸發(好比在方法pub.DoSomething()中),換言之,就是NumberChanged(100)語句只能在Publisher 內部被調用。你們能夠嘗試一下,將委託變量的聲明那行代碼註釋掉,而後取消下面事件聲明的註釋。此時程序是沒法編譯的,當你使用了event 關鍵字以後,直接在客戶端觸發事件這種行爲,也就是直接調用pub.NumberChanged(100),是被禁止的。事件只能經過調用DoSomething() 來觸發。這樣纔是事件的本意,事件發佈者的封裝纔會更好。

    就好像若是咱們要定義一個數字類型,咱們會使用int 而不是使用object 同樣,給予對象過多的能力並不見得是一件好事,應該是越合適越好。儘管直接使用委託變量一般不會有什麼問題,但它給了客戶端不該具備的能力,而使用事件,能夠限制這一能力,更精確地對類型進行封裝。

    約定

    這裏還有一個約定俗稱的規定,就是訂閱事件的方法的命名,一般爲"On 事件名",好比這裏的OnNumberChanged

    12.3 Observer 設計模式

    如今假設咱們有個高檔的熱水器,熱水器由三部分組成:熱水器、警報器、顯示器,它們來自於不一樣廠商並進行了組裝。給熱水器通上電,當水溫超過95度的時候:揚聲器會開始發出語音,告訴你水的溫度;液晶屏也會改變水溫的顯示,來提示水已經快燒開了。

    如今咱們須要寫個程序來模擬這個燒水的過程。

    經過對上面需求的分析,咱們會定義熱水器、警報器和顯示器三個類。如示例15所示。

    示例15

    // 熱水器類

    public class Heater

    {

    private int temperature; //溫度

    // 燒水

    private void BoilWater()

    {

    for (int i = 0; i <= 100; i++)

    {

    temperature = i;

    }

    }

    }

    // 警報器類

    public class Alarm

    {

    // 發出語言警報方法

    private void MakeAlert(int param)

    {

    Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);

    }

    }

       

    // 顯示器類

    public class Display

    {

    // 顯示溫度方法

    private void ShowMsg(int param)

    {

    Console.WriteLine("Display:水已燒開,當前溫度:{0}度。", param);

    }

    }

    這裏就出現了一個問題:如何在水燒開的時候通知報警器和顯示器?

    在繼續進行以前,咱們先了解一下Observer設計模式(觀察者模式),Observer 設計模式中主要包括以下兩類對象:

  • Subject:監視對象,它每每包含着其餘對象所感興趣的內容。
    示例15中,熱水器就是一個監視對象,它包含的其餘對象所感興趣的內容,就是 temprature 字段,當這個字段的值快到100 時,會不斷把數據發給監視它的對象。
  • Observer:監視者,它監視 Subject,當 Subject 中的某件事發生的時候,會告知 Observer,而 Observer 則會採起相應的行動。
    示例 15中, Observer有警報器和顯示器,它們採起的行動分別是發出警報和顯示水溫。

    在本例中,事情發生的順序應該是這樣的:

    1. 警報器和顯示器告訴熱水器,它對它的溫度比較感興趣(註冊)

    2. 熱水器知道後保留對警報器和顯示器的引用。

    3. 熱水器進行燒水這一動做,當水溫超過 95 度時,經過對警報器和顯示器的引用,自動調用警報器的MakeAlert()方法、顯示器的ShowMsg()方法。

    Observer 設計模式

    Observer 設計模式是爲了定義對象間的一種一對多的依賴關係,以便於當一個對象的狀態改變時,其餘依賴於它的對象會被自動告知並更新。

    按照Observer設計模式,咱們對熱水器類進行修改,如示例16所示。

    示例16

    // 熱水器類

    public class Heate

    {

    //溫度

    private int temperature;

       

    public delegate void BoilHandler(int param);

    // 定義事件

    public event BoilHandler BoilEvent;

    // 燒水

    public void BoilWater()

    {

    for (int i = 0; i <= 100; i++)

    {

    temperature = i;

    if (temperature > 95)

    {

    if (BoilEvent != null)

    BoilEvent(temperature); // 調用全部註冊對象的方法

    }

    }

    }

    }

    //測試類Main()方法

    static void Main()

    {

    Heater heater = new Heater();

    Alarm alarm = new Alarm();

    heater.BoilEvent += alarm.MakeAlert; // 註冊方法

    heater.BoilEvent += (new Alarm()).MakeAlert; // 給匿名對象註冊方法

    heater.BoilEvent += Display.ShowMsg; // 註冊靜態方法

    heater.BoilWater(); // 燒水,會自動調用註冊過對象的方法

    }

    運行結果如圖12-5所示。

     

     

    12-5 熱水器燒水運行結果

相關文章
相關標籤/搜索