1. 程式人生 > >【轉載】C# 中的委託和事件(詳解:簡單易懂的講解) C# 中的委託和事件(詳解)

【轉載】C# 中的委託和事件(詳解:簡單易懂的講解) C# 中的委託和事件(詳解)

本文轉載自http://www.cnblogs.com/SkySoot/archive/2012/04/05/2433639.html

C# 中的委託和事件(詳解)

C# 中的委託和事件

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

1.1 理解委託

1.1.1 將方法作為方法的引數

我們先不管這個標題如何的繞口,也不管委託究竟是個什麼東西,來看下面這兩個最簡單的方法,它們不過是在螢幕上輸出一句問候的話語:

public void GreetPeople(string name)
{
    EnglishGreeting(name);
}
 
public void EnglishGreeting(string name)
{
    Console.WriteLine("Good Morning, " + name);
}

暫且不管這兩個方法有沒有什麼實際意義。GreetPeople 用於向某人問好,當我們傳遞代表某人姓名的 name 引數,比如說“Liker”進去的時候,在這個方法中,將呼叫 EnglishGreeting 方法,再次傳遞 name 引數,EnglishGreeting 則用於向螢幕輸出 “Good Morning, Liker”。

現在假設這個程式需要進行全球化,哎呀,不好了,我是中國人,我不明白“Good Morning”是什麼意思,怎麼辦呢?好吧,我們再加個中文版的問候方法:

public void ChineseGreeting(string name)
{
    Console.WriteLine("早上好, " + name);
}

這時候,GreetPeople 也需要改一改了,不然如何判斷到底用哪個版本的 Greeting 問候方法合適呢?在進行這個之前,我們最好再定義一個列舉作為判斷的依據:

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;
    }
}

OK,儘管這樣解決了問題,但我不說大家也很容易想到,這個解決方案的可擴充套件性很差,如果日後我們需要再新增韓文版、日文版,就不得不反覆修改列舉和GreetPeople() 方法,以適應新的需求。

在考慮新的解決方案之前,我們先看看 GreetPeople 的方法簽名:

       public void GreetPeople(string name, Language lang);

我們僅看 string name,在這裡,string 是引數型別,name 是引數變數,當我們賦給 name 字串“Liker”時,它就代表“Liker”這個值;當我們賦給它“李志中”時,它又代表著“李志中”這個值。然後,我們可以在方法體內對這個 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,而在這個兩個方法內部,已經對使用“Good Morning”還是“早上好”作了區分。

聰明的你應該已經想到了,現在是委託該出場的時候了,但講述委託之前,我們再看看MakeGreeting 引數所能代表的 ChineseGreeting()和EnglishGreeting()方法的簽名:

public void EnglishGreeting(string name)

public void ChineseGreeting(string name)

如同 name 可以接受 String 型別的“true”和“1”,但不能接受bool 型別的true 和int 型別的1 一樣。MakeGreeting 的引數型別定義應該能夠確定 MakeGreeting 可以代表的方法種類,再進一步講,就是 MakeGreeting 可以代表的方法的引數型別和返回型別。

於是,委託出現了:它定義了 MakeGreeting 引數所能代表的方法的種類,也就是 MakeGreeting 引數的型別。

本例中委託的定義:

    public delegate void GreetingDelegate(string name);

與上面 EnglishGreeting() 方法的簽名對比一下,除了加入了delegate 關鍵字以外,其餘的是不是完全一樣?現在,讓我們再次改動GreetPeople()方法,如下所示:

public delegate void GreetingDelegate(string name);
public void GreetPeople(string name, GreetingDelegate MakeGreeting)
{
    MakeGreeting(name);
}

如你所見,委託 GreetingDelegate 出現的位置與 string 相同,string 是一個型別,那麼 GreetingDelegate 應該也是一個型別,或者叫類(Class)。但是委託的宣告方式和類卻完全不同,這是怎麼一回事?實際上,委託在編譯的時候確實會編譯成類。因為 Delegate 是一個類,所以在任何可以宣告類的地方都可以宣告委託。更多的內容將在下面講述,現在,請看看這個範例的完整程式碼:

public delegate void GreetingDelegate(string name);
 
class Program
{
    private static void EnglishGreeting(string name)
    {
        Console.WriteLine("Good Morning, " + name);
    }
 
    private static void ChineseGreeting(string name)
    {
        Console.WriteLine("早上好, " + name);
    }
 
    private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
    {
        MakeGreeting(name);
    }
 
    static void Main(string[] args)
    {
        GreetPeople("Liker", EnglishGreeting);
        GreetPeople("李志中", ChineseGreeting);
        Console.ReadLine();
    }
}

我們現在對委託做一個總結:委託是一個類,它定義了方法的型別,使得可以將方法當作另一個方法的引數來進行傳遞,這種將方法動態地賦給引數的做法,可以避免在程式中大量使用If … Else(Switch)語句,同時使得程式具有更好的可擴充套件性。

1.1.2 將方法繫結到委託

看到這裡,是不是有那麼點如夢初醒的感覺?於是,你是不是在想:在上面的例子中,我不一定要直接在 GreetPeople() 方法中給 name 引數賦值,我可以像這樣使用變數:

static void Main(string[] args)
{
    GreetPeople("Liker", EnglishGreeting);
    GreetPeople("李志中", ChineseGreeting);
    Console.ReadLine();
}

而既然委託 GreetingDelegate 和型別 string 的地位一樣,都是定義了一種引數型別,那麼,我是不是也可以這麼使用委託?

static void Main(string[] args)
{
    GreetingDelegate delegate1, delegate2;
    delegate1 = EnglishGreeting;
    delegate2 = ChineseGreeting;
    GreetPeople("Liker", delegate1);
    GreetPeople("李志中", delegate2);
    Console.ReadLine();
}

如你所料,這樣是沒有問題的,程式一如預料的那樣輸出。這裡,我想說的是委託不同於 string 的一個特性:可以將多個方法賦給同一個委託,或者叫將多個方法繫結到同一個委託,當呼叫這個委託的時候,將依次呼叫其所繫結的方法。在這個例子中,語法如下:

static void Main(string[] args)
{
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; 
    delegate1 += ChineseGreeting;
    GreetPeople("Liker", delegate1);
    Console.ReadLine();
}

實際上,我們可以也可以繞過GreetPeople 方法,通過委託來直接呼叫EnglishGreeting 和ChineseGreeting:

static void Main(string[] args)
{
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting; 
    delegate1("Liker");
    Console.ReadLine();
}

說明:這在本例中是沒有問題的,但回頭看下上面 GreetPeople() 的定義,在它之中可以做一些對於 EnglshihGreeting 和 ChineseGreeting 來說都需要進行的工作,為了簡便我做了省略。

注意這裡,第一次用的“=”,是賦值的語法;第二次,用的是“+=”,是繫結的語法。如果第一次就使用“+=”,將出現“使用了未賦值的區域性變數”的編譯錯誤。我們也可以使用下面的程式碼來這樣簡化這一過程:

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
delegate1 += ChineseGreeting;

既然給委託可以繫結一個方法,那麼也應該有辦法取消對方法的繫結,很容易想到,這個語法是“-=”:

static void Main(string[] args)
{
    GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
    delegate1 += ChineseGreeting;
    GreetPeople("Liker", delegate1);
    Console.WriteLine();
    
    delegate1 -= EnglishGreeting;
    GreetPeople("李志中", delegate1);
    Console.ReadLine();
}

讓我們再次對委託作個總結:

    使用委託可以將多個方法繫結到同一個委託變數,當呼叫此變數時(這裡用“呼叫”這個詞,是因為此變數代表一個方法),可以依次呼叫所有繫結的方法。

 

1.2 事件的由來

1.2.1 更好的封裝性

我們繼續思考上面的程式:上面的三個方法都定義在 Programe 類中,這樣做是為了理解的方便,實際應用中,通常都是 GreetPeople 在一個類中,ChineseGreeting 和 EnglishGreeting 在另外的類中。現在你已經對委託有了初步瞭解,是時候對上面的例子做個改進了。假設我們將 GreetingPeople() 放在一個叫 GreetingManager 的類中,那麼新程式應該是這個樣子的:

namespace Delegate
{
    public delegate void GreetingDelegate(string name);
  
    public class GreetingManager
    {
        public void GreetPeople(string name, GreetingDelegate MakeGreeting)
        {
            MakeGreeting(name);
        }
    }
 
    class Program
    {
        private static void EnglishGreeting(string name)
        {
            Console.WriteLine("Good Morning, " + name);
        }
 
        private static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好, " + name);
        }
 
        static void Main(string[] args)
        {
            GreetingManager gm = new GreetingManager();
            gm.GreetPeople("Liker", EnglishGreeting);
            gm.GreetPeople("李志中", ChineseGreeting);
        }
    }
}

我們執行這段程式碼,嗯,沒有任何問題。程式一如預料地那樣輸出了:

// ************************************************************************

Good Morning, Liker

早上好, 李志中

// ************************************************************************

 

現在,假設我們需要使用上一節學到的知識,將多個方法繫結到同一個委託變數,該如何做呢?讓我們再次改寫程式碼:

static void Main(string[] args)
{
    GreetingManager gm = new GreetingManager();
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting;
    gm.GreetPeople("Liker", delegate1);
}

輸出:

Good Morning, Liker

早上好, Liker

 

到了這裡,我們不禁想到:面向物件設計,講究的是物件的封裝,既然可以宣告委託型別的變數(在上例中是delegate1),我們何不將這個變數封裝到 GreetManager 類中?在這個類的客戶端中使用不是更方便麼?於是,我們改寫GreetManager 類,像這樣:

public class GreetingManager
{
    /// <summary>
    /// 在 GreetingManager 類的內部宣告 delegate1 變數
    /// </summary>
    public GreetingDelegate delegate1;
 
    public void GreetPeople(string name, GreetingDelegate MakeGreeting)
    {
        MakeGreeting(name);
    }
}

現在,我們可以這樣使用這個委託變數:

static void Main(string[] args)
{
    GreetingManager gm = new GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;
    gm.GreetPeople("Liker", gm.delegate1);
}

輸出為:

Good Morning, Liker

早上好, Liker

儘管這樣做沒有任何問題,但我們發現這條語句很奇怪。在呼叫gm.GreetPeople 方法的時候,再次傳遞了gm 的delegate1 欄位,既然如此,我們何不修改 GreetingManager 類成這樣:

public class GreetingManager
 {
     /// <summary>
     /// 在 GreetingManager 類的內部宣告 delegate1 變數
     /// </summary>
     public GreetingDelegate delegate1;
 
     public void GreetPeople(string name)
     {
         if (delegate1 != null) // 如果有方法註冊委託變數
         { 
             delegate1(name); // 通過委託呼叫方法
         }
     }
 }

在客戶端,呼叫看上去更簡潔一些:

static void Main(string[] args)
{
    GreetingManager gm = new GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;
    gm.GreetPeople("Liker"); //注意,這次不需要再傳遞 delegate1 變數
}

儘管這樣達到了我們要的效果,但是還是存在著問題:在這裡,delegate1 和我們平時用的string 型別的變數沒有什麼分別,而我們知道,並不是所有的欄位都應該宣告成public,合適的做法是應該public 的時候public,應該private 的時候private。

我們先看看如果把 delegate1 宣告為 private 會怎樣?結果就是:這簡直就是在搞笑。因為宣告委託的目的就是為了把它暴露在類的客戶端進行方法的註冊,你把它宣告為 private 了,客戶端對它根本就不可見,那它還有什麼用?

再看看把delegate1 宣告為 public 會怎樣?結果就是:在客戶端可以對它進行隨意的賦值等操作,嚴重破壞物件的封裝性。

最後,第一個方法註冊用“=”,是賦值語法,因為要進行例項化,第二個方法註冊則用的是“+=”。但是,不管是賦值還是註冊,都是將方法繫結到委託上,除了呼叫時先後順序不同,再沒有任何的分別,這樣不是讓人覺得很彆扭麼?

現在我們想想,如果delegate1 不是一個委託型別,而是一個string 型別,你會怎麼做?答案是使用屬性對欄位進行封裝。

於是,Event 出場了,它封裝了委託型別的變數,使得:在類的內部,不管你宣告它是public還是protected,它總是private 的。在類的外部,註冊“+=”和登出“-=”的訪問限定符與你在宣告事件時使用的訪問符相同。我們改寫GreetingManager 類,它變成了這個樣子:

public class GreetingManager
{
    //這一次我們在這裡宣告一個事件
    public event GreetingDelegate MakeGreet;
 
    public void GreetPeople(string name)
    {
        MakeGreet(name);
    }
}

很容易注意到:MakeGreet 事件的宣告與之前委託變數 delegate1 的宣告唯一的區別是多了一個 event 關鍵字。看到這裡,在結合上面的講解,你應該明白到:事件其實沒什麼不好理解的,宣告一個事件不過類似於宣告一個進行了封裝的委託型別的變數而已。

為了證明上面的推論,如果我們像下面這樣改寫Main 方法:

static void Main(string[] args)
{
    GreetingManager gm = new GreetingManager();
    gm.MakeGreet = EnglishGreeting; // 編譯錯誤1
    gm.MakeGreet += ChineseGreeting;
    gm.GreetPeople("Liker");
}

會得到編譯錯誤:

image

1.2.2 限制類型能力

使用事件不僅能獲得比委託更好的封裝性以外,還能限制含有事件的型別的能力。這是什麼意思呢?它的意思是說:事件應該由事件釋出者觸發,而不應該由事件的客戶端(客戶程式)來觸發。請看下面的範例:

using System;
 
class Program
{
    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); // 但可以被這樣直接呼叫,對委託變數的不恰當使用
    }
}
 
/// <summary>
/// 定義委託
/// </summary>
/// <param name="count"></param>
public delegate void NumberChangedEventHandler(int count);
 
/// <summary>
/// 定義事件釋出者
/// </summary>
public class Publishser
{
    private int count;
 
    public NumberChangedEventHandler NumberChanged; // 宣告委託變數
 
    //public event NumberChangedEventHandler NumberChanged; // 宣告一個事件
 
    public void DoSomething()
    {
        // 在這裡完成一些工作 ...
 
        if (NumberChanged != null) // 觸發事件
        { 
            count++;
            NumberChanged(count);
        }
    }
}
 
/// <summary>
/// 定義事件訂閱者
/// </summary>
public class Subscriber
{
    public void OnNumberChanged(int count)
    {
        Console.WriteLine("Subscriber notified: count = {0}", count);
    }
}
 
 

上面程式碼定義了一個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。

 

1.3 委託的編譯程式碼

這時候,我們註釋掉編譯錯誤的行,然後重新進行編譯,再借助 Reflactor 來對 event 的宣告語句做一探究,看看為什麼會發生這樣的錯誤:

clip_image002

可以看到,實際上儘管我們在GreetingManager 裡將 MakeGreet 宣告為public,但是,實際上MakeGreet 會被編譯成私有欄位,難怪會發生上面的編譯錯誤了,因為它根本就不允許在GreetingManager 類的外面以賦值的方式訪問,從而驗證了我們上面所做的推論。

我們再進一步看下MakeGreet 所產生的程式碼:

// ************************************************************************

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_MakeGreet和remove_MakeGreet,這兩個方法分別用於註冊委託型別的方法和取消註冊。實際上也就是:“+= ”對應 add_MakeGreet,“-=”對應remove_MakeGreet。而這兩個方法的訪問限制取決於宣告事件時的訪問限制符。

在add_MakeGreet()方法內部,實際上呼叫了System.Delegate 的Combine()靜態方法,這個方法用於將當前的變數新增到委託連結串列中。

我們前面提到過兩次,說委託實際上是一個類,在我們定義委託的時候:

// ************************************************************************

public delegate void GreetingDelegate(string name);

// ************************************************************************

當編譯器遇到這段程式碼的時候,會生成下面這樣一個完整的類:

// ************************************************************************

public class GreetingDelegate:System.MulticastDelegate

{

public GreetingDelegate(object @object, IntPtr method);

public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);

public virtual void EndInvoke(IAsyncResult result);

public virtual void Invoke(string name);

}

// ************************************************************************

clip_image004

 

1.4 .NET 框架中的委託和事件

1.4.1 範例說明

上面的例子已不足以再進行下面的講解了,我們來看一個新的範例,因為之前已經介紹了很多的內容,所以本節的進度會稍微快一些!

假設我們有個高檔的熱水器,我們給它通上電,當水溫超過95 度的時候:1、揚聲器會開始發出語音,告訴你水的溫度;2、液晶屏也會改變水溫的顯示,來提示水已經快燒開了。

現在我們需要寫個程式來模擬這個燒水的過程,我們將定義一個類來代表熱水器,我們管它叫:Heater,它有代表水溫的欄位,叫做 temperature;當然,還有必不可少的給水加熱方法 BoilWater(),一個發出語音警報的方法 MakeAlert(),一個顯示水溫的方法,ShowMsg()。

namespace Delegate
{
    /// <summary>
    /// 熱水器
    /// </summary>
    public class Heater
    {
        /// <summary>
        /// 水溫
        /// </summary>
        private int temperature;
 
        /// <summary>
        /// 燒水
        /// </summary>
        public void BoilWater()
        {
            for (int i = 0; i <= 100; i++)
            {
                temperature = i;
                if (temperature > 95)
                {
                    MakeAlert(temperature);
                    ShowMsg(temperature);
                }
            }
        }
        
        /// <summary>
        /// 發出語音警報
        /// </summary>
        /// <param name="param"></param>
        private void MakeAlert(int param)
        {
            Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);
        }
 
        /// <summary>
        /// 顯示水溫
        /// </summary>
        /// <param name="param"></param>
        private void ShowMsg(int param)
        {
            Console.WriteLine("Display:水快開了,當前溫度:{0}度。", param);
        }
    }
 
    class Program
    {
        static void Main()
        {
            Heater ht = new Heater();
            ht.BoilWater();
        }
    }
}
1.4.2 Observer 設計模式簡介

上面的例子顯然能完成我們之前描述的工作,但是卻並不夠好。現在假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自於不同廠商並進行了組裝。那麼,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。

這時候,上面的例子就應該變成這個樣子:

/// <summary>
/// 熱水器
/// </summary>
public class Heater
{
    private int temperature;    
 
    private void BoilWater()
    {
        for (int i = 0; i <= 100; i++)
        {
            temperature = i;
        }
    }
}
 
/// <summary>
/// 警報器
/// </summary>
public class Alarm
{
    private void MakeAlert(int param)
    {
        Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);
    }
}
 
/// <summary>
/// 顯示器
/// </summary>
public class Display
{
    private void ShowMsg(int param)
    {
        Console.WriteLine("Display:水已燒開,當前溫度:{0}度。", param);
    }
}

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

在繼續進行之前,我們先了解一下Observer 設計模式,Observer 設計模式中主要包括如下兩類物件:

Subject:監視物件,它往往包含著其他物件所感興趣的內容。在本範例中,熱水器就是一個監視物件,它包含的其他物件所感興趣的內容,就是 temprature 欄位,當這個欄位的值快到100 時,會不斷把資料發給監視它的物件。

Observer:監視者,它監視Subject,當 Subject 中的某件事發生的時候,會告知Observer,而Observer 則會採取相應的行動。在本範例中,Observer 有警報器和顯示器,它們採取的行動分別是發出警報和顯示水溫。

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

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

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

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

 

類似這樣的例子是很多的,GOF 對它進行了抽象,稱為 Observer 設計模式:Observer 設計模式是為了定義物件間的一種一對多的依賴關係,以便於當一個物件的狀態改變時,其他依賴於它的物件會被自動告知並更新。Observer 模式是一種鬆耦合的設計模式。

 

1.4.3 實現範例的Observer 設計模式

我們之前已經對委託和事件介紹很多了,現在寫程式碼應該很容易了,現在在這裡直接給出程式碼,並在註釋中加以說明。

namespace Delegate
{
    public class Heater
    {
        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); // 呼叫所有註冊物件的方法
                    }
                }
            }
        }
    }
 
    public class Alarm
    {
        public void MakeAlert(int param)
        {
            Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);
        }
    }
 
    public class Display
    {
        public static void ShowMsg(int param) // 靜態方法
        { 
            Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", param);
        }
    }
 
    class Program
    {
        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(); // 燒水,會自動呼叫註冊過物件的方法
        }
    }
}

輸出為:

// ************************************************************************

Alarm:嘀嘀嘀,水已經 96 度了:

Alarm:嘀嘀嘀,水已經 96 度了:

Display:水快燒開了,當前溫度:96 度。

// 省略...

// ************************************************************************

 

1.4.4 .NET 框架中的委託與事件

儘管上面的範例很好地完成了我們想要完成的工作,但是我們不僅疑惑:為什麼.NET Framework 中的事件模型和上面的不同?為什麼有很多的EventArgs 引數?

在回答上面的問題之前,我們先搞懂 .NET Framework 的編碼規範:

1. 委託型別的名稱都應該以 EventHandler 結束。

2. 委託的原型定義:有一個void 返回值,並接受兩個輸入引數:一個Object 型別,一個EventArgs 型別(或繼承自EventArgs)。

3. 事件的命名為委託去掉 EventHandler 之後剩餘的部分。

4. 繼承自 EventArgs 的型別應該以EventArgs 結尾。

再做一下說明:

1. 委託宣告原型中的Object 型別的引數代表了Subject,也就是監視物件,在本例中是Heater(熱水器)。回撥函式(比如Alarm 的MakeAlert)可以通過它訪問觸發事件的物件(Heater)。

2. EventArgs 物件包含了Observer 所感興趣的資料,在本例中是temperature。

上面這些其實不僅僅是為了編碼規範而已,這樣也使得程式有更大的靈活性。比如說,如果我們不光想獲得熱水器的溫度,還想在Observer 端(警報器或者顯示器)方法中獲得它的生產日期、型號、價格,那麼委託和方法的宣告都會變得很麻煩,而如果我們將熱水器的引用傳給警報器的方法,就可以在方法中直接訪問熱水器了。

現在我們改寫之前的範例,讓它符合.NET Framework的規範:

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Delegate
{
    public class Heater
    {
        private int temperature;
        public string type = "RealFire 001"; // 新增型號作為演示
        public string area = "China Xian"; // 新增產地作為演示
 
        public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);
 
        public event BoiledEventHandler Boiled; // 宣告事件
 
        // 定義 BoiledEventArgs 類,傳遞給 Observer 所感興趣的資訊
        public class BoiledEventArgs : EventArgs
        {
            public readonly int temperature;
            public BoiledEventArgs(int temperature)
            {
                this.temperature = temperature;
            }
        }
 
        // 可以供繼承自 Heater 的類重寫,以便繼承類拒絕其他物件對它的監視
        protected virtual void OnBoiled(BoiledEventArgs e)
        {
            if (Boiled != null)
            {
                Boiled(this, e); // 呼叫所有註冊物件的方法
            }
        }
 
        public void BoilWater()
        {
            for (int i = 0; i <= 100; i++)
            {
                temperature = i;
                if (temperature > 95)
                {
                    // 建立BoiledEventArgs 物件。
 
                    BoiledEventArgs e = new BoiledEventArgs(temperature);
                    OnBoiled(e); // 呼叫 OnBolied 方法
                }
            }
        }
 
        public class Alarm
        {
            public void MakeAlert(Object sender, Heater.BoiledEventArgs e)
            {
                Heater heater = (Heater)sender; // 這裡是不是很熟悉呢?
 
                // 訪問 sender 中的公共欄位
                Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);
                Console.WriteLine("Alarm: 嘀嘀嘀,水已經 {0} 度了:", e.temperature);
                Console.WriteLine();
            }
        }
 
        public class Display
        {
            public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) // 靜態方法
            {
                Heater heater = (Heater)sender;
                Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
                Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", e.temperature);
                Console.WriteLine();
            }
        }
 
        class Program
        {
            static void Main()
            {
                Heater heater = new Heater();
                Alarm alarm = new Alarm();
                heater.Boiled += alarm.MakeAlert; //註冊方法
                heater.Boiled += (new Alarm()).MakeAlert; //給匿名物件註冊方法
                heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert); //也可以這麼註冊
                heater.Boiled += Display.ShowMsg; //註冊靜態方法
                heater.BoilWater(); //燒水,會自動呼叫註冊過物件的方法
            }
        }
    }
}

輸出為:

Alarm:China Xian - RealFire 001:

Alarm: 嘀嘀嘀,水已經 96 度了:

Alarm:China Xian - RealFire 001:

Alarm: 嘀嘀嘀,水已經 96 度了:

Alarm:China Xian - RealFire 001:

Alarm: 嘀嘀嘀,水已經 96 度了:

Display:China Xian - RealFire 001:

Display:水快燒開了,當前溫度:96 度。

// 省略 ...

 

1.5 委託進階

1.5.1 為什麼委託定義的返回值通常都為 void ?

儘管並非必需,但是我們發現很多的委託定義返回值都為 void,為什麼呢?這是因為委託變數可以供多個訂閱者註冊,如果定義了返回值,那麼多個訂閱者的方法都會向釋出者返回數值,結果就是後面一個返回的方法值將前面的返回值覆蓋掉了,因此,實際上只能獲得最後一個方法呼叫的返回值。可以執行下面的程式碼測試一下。除此以外,釋出者和訂閱者是鬆耦合的,釋出者根本不關心誰訂閱了它的事件、為什麼要訂閱,更別說訂閱者的返回值了,所以返回訂閱者的方法返回值大多數情況下根本沒有必要。

1.5.2 如何讓事件只允許一個客戶訂閱?

少數情況下,比如像上面,為了避免發生“值覆蓋”的情況(更多是在非同步呼叫方法時,後面會討論),我們可能想限制只允許一個客戶端註冊。此時怎麼做呢?我們可以向下面這樣,將事件宣告為private 的,然後提供兩個方法來進行註冊和取消註冊:

public class Publishser
{
 
    private event GeneralEventHandler NumberChanged; // 宣告一個私有事件
 
    // 註冊事件
    public void Register(GeneralEventHandler method)
    {
        NumberChanged = method;
    }
    
    // 取消註冊
    public void UnRegister(GeneralEventHandler method)
    {
        NumberChanged -= method;
    }
 
    public void DoSomething()
    {
        // 做某些其餘的事情
        if (NumberChanged != null)
        { // 觸發事件
            string rtn = NumberChanged();
            Console.WriteLine("Return: {0}", rtn); // 列印返回的字串,輸出為Subscriber3
        }
    }
}

注意上面,在UnRegister()中,沒有進行任何判斷就使用了NumberChanged -= method 語句。這是因為即使method 方法沒有進行過註冊,此行語句也不會有任何問題,不會丟擲異常,僅僅是不會產生任何效果而已。

注意在Register()方法中,我們使用了賦值操作符“=”,而非“+=”,通過這種方式就避免了多個方法註冊。

 

1.7 委託和方法的非同步呼叫

通常情況下,如果需要非同步執行一個耗時的操作,我們會新起一個執行緒,然後讓這個執行緒去執行程式碼。但是對於每一個非同步呼叫都通過建立執行緒來進行操作顯然會對效能產生一定的影響,同時操作也相對繁瑣一些。.NET 中可以通過委託進行方法的非同步呼叫,就是說客戶端在非同步呼叫方法時,本身並不會因為方法的呼叫而中斷,而是從執行緒池中抓取一個執行緒去執行該方法,自身執行緒(主執行緒)在完成抓取執行緒這一過程之後,繼續執行下面的程式碼,這樣就實現了程式碼的並行執行。使用執行緒池的好處就是避免了頻繁進行非同步呼叫時建立、銷燬執行緒的開銷。當我們在委託物件上呼叫BeginInvoke()時,便進行了一個非同步的方法呼叫。

事件釋出者和訂閱者之間往往是鬆耦合的,釋出者通常不需要獲得訂閱者方法執行的情況;而當使用非同步呼叫時,更多情況下是為了提升系統的效能,而並非專用於事件的釋出和訂閱這一程式設計模型。而在這種情況下使用非同步程式設計時,就需要進行更多的控制,比如當非同步執行方法的方法結束時通知客戶端、返回非同步執行方法的返回值等。本節就對 BeginInvoke() 方法、EndInvoke() 方法和其相關的 IAysncResult 做一個簡單的介紹。

我們先看這樣一段程式碼,它演示了不使用非同步呼叫的通常情況:

class Program7
{
    static void Main(string[] args)
    {
        Console.WriteLine("Client application started!\n");
        Thread.CurrentThread.Name = "Main Thread";
        Calculator cal = new Calculator();
        int result = cal.Add(2, 5);
        Console.WriteLine("Result: {0}\n", result);
 
        // 做某些其它的事情,模擬需要執行3 秒鐘
        for (int i = 1; i <= 3; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Client executed {1} second(s).", Thread.CurrentThread.Name, i);
        }
 
        Console.WriteLine("\nPress any key to exit...");
        Console.ReadLine();
    }
}
 
public class Calculator
{
    public int Add(int x, int y)
    {
        if (Thread.CurrentThread.IsThreadPoolThread)
        {
            Thread.CurrentThread.Name = "Pool Thread";
        }
 
        Console.WriteLine("Method invoked!");
 
        // 執行某些事情,模擬需要執行2 秒鐘
        for (int i = 1; i <= 2; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Add executed {1} second(s).", Thread.CurrentThread.Name, i);
        }
 
        Console.WriteLine("Method complete!");
        return x + y;
    }
}

上面程式碼有幾個關於對於執行緒的操作,如果不瞭解可以看一下下面的說明,如果你已經瞭解可以直接跳過:

1. Thread.Sleep(),它會讓執行當前程式碼的執行緒暫停一段時間(如果你對執行緒的概念比較陌生,可以理解為使程式的執行暫停一段時間),以毫秒為單位,比如Thread.Sleep(1000),將會使執行緒暫停1 秒鐘。在上面我使用了它的過載方法,個人覺得使用TimeSpan.FromSeconds(1),可讀性更好一些。

2. Thread.CurrentThread.Name,通過這個屬性可以設定、獲取執行當前程式碼的執行緒的名稱,值得注意的是這個屬性只可以設定一次,如果設定兩次,會丟擲異常。

3. Thread.IsThreadPoolThread,可以判斷執行當前程式碼的執行緒是否為執行緒池中的執行緒。

通過這幾個方法和屬性,有助於我們更好地除錯非同步呼叫方法。上面程式碼中除了加入了一些對執行緒的操作以外再沒有什麼特別之處。我們建了一個Calculator 類,它只有一個Add 方法,我們模擬了這個方法需要執行2 秒鐘時間,並且每隔一秒進行一次輸出。而在客戶端程式中,我們使用result 變數儲存了方法的返回值並進行了列印。隨後,我們再次模擬了客戶端程式接下來的操作需要執行2 秒鐘時間。執行這段程式,會產生下面的輸出:

// ************************************************************************

Client application started!

Method invoked!

Main Thread: Add executed 1 second(s).

Main Thread: Add executed 2 second(s).

Method complete!

Result: 7

Main Thread: Client executed 1 second(s).

Main Thread: Client executed 2 second(s).

Main Thread: Client executed 3 second(s).

Press any key to exit...

// ************************************************************************

如果你確實執行了這段程式碼,會看到這些輸出並不是一瞬間輸出的,而是執行了大概5 秒鐘的時間,因為執行緒是序列執行的,所以在執行完 Add() 方法之後才會繼續客戶端剩下的程式碼。

接下來我們定義一個AddDelegate 委託,並使用BeginInvoke()方法來非同步地呼叫它。在上面已經介紹過,BeginInvoke()除了最後兩個引數為AsyncCallback 型別和Object 型別以外,前面的引數型別和個數與委託定義相同。另外BeginInvoke()方法返回了一個實現了IAsyncResult 介面的物件(實際上就是一個AsyncResult 型別例項,注意這裡IAsyncResult 和AysncResult 是不同的,它們均包含在.NET Framework 中)。

AsyncResult 的用途有這麼幾個:傳遞引數,它包含了對呼叫了BeginInvoke()的委託的引用;它還包含了BeginInvoke()的最後一個Object 型別的引數;它可以鑑別出是哪個方法的哪一次呼叫,因為通過同一個委託變數可以對同一個方法呼叫多次。

EndInvoke()方法接受IAsyncResult 型別的物件(以及ref 和out 型別引數,這裡不討論了,對它們的處理和返回值類似),所以在呼叫BeginInvoke()之後,我們需要保留IAsyncResult,以便在呼叫EndInvoke()時進行傳遞。這裡最重要的就是EndInvoke()方法的返回值,它就是方法的返回值。除此以外,當客戶端呼叫EndInvoke()時,如果非同步呼叫的方法沒有執行完畢,則會中斷當前執行緒而去等待該方法,只有當非同步方法執行完畢後才會繼續執行後面的程式碼。所以在呼叫完BeginInvoke()後立即執行EndInvoke()是沒有任何意義的。我們通常在儘可能早的時候呼叫BeginInvoke(),然後在需要方法的返回值的時候再去呼叫EndInvoke(),或者是根據情況在晚些時候呼叫。說了這麼多,我們現在看一下使用非同步呼叫改寫後上面的程式碼吧:

using System.Threading;
using System;
 
public delegate int AddDelegate(int x, int y);
class Program8
{
    static void Main(string[] args)
    {
        Console.WriteLine("Client application started!\n");
        Thread.CurrentThread.Name = "Main Thread";
        Calculator cal = new Calculator();
        AddDelegate del = new AddDelegate(cal.Add);
        IAsyncResult asyncResult = del.BeginInvoke(2, 5, null, null); // 非同步呼叫方法
 
        // 做某些其它的事情,模擬需要執行3 秒鐘
        for (int i = 1; i <= 3; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Client executed {1} second(s).", Thread.CurrentThread.Name, i);
        }
        int rtn = del.EndInvoke(asyncResult);
        Console.WriteLine("Result: {0}\n", rtn);
        Console.WriteLine("\nPress any key to exit...");
        Console.ReadLine();
    }
}
 
public class Calculator
{
    public int Add(int x, int y)
    {
        if (Thread.CurrentThread.IsThreadPoolThread)
        {
            Thread.CurrentThread.Name = "Pool Thread";
        }
 
        Console.WriteLine("Method invoked!");
 
        // 執行某些事情,模擬需要執行2 秒鐘