C#中委托和事件


目  錄

1.1 理解委托    2
1.1.1 將方法作為方法的參數    2
1.1.2 將方法綁定到委托    4
1.2 事件的由來    6
1.2.1 更好的封裝性    6
1.2.2 限制類型能力    9
1.3 委托的編譯代碼    10
1.4 .NET 框架中的委托和事件    11
1.4.1 范例說明    11
1.4.2 Observer 設計模式簡介    12
1.4.3 實現范例的Observer 設計模式    13
1.4.4 .NET 框架中的委托與事件    14
1.5 委托進階    16
1.5.1 為什么委托定義的返回值通常都為void?    16
1.5.2 如何讓事件只允許一個客戶訂閱?    16
1.5.3 獲得多個返回值與異常處理    18
1.6 訂閱者方法超時的處理    21
1.7 委托和方法的異步調用    24
1.8 總結    28








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) { //做某些額外的事情,比如初始化之類,此處略 swith(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參數的類型。 說 明:如果上面這句話比較繞口,我把它翻譯成這樣:
string 定義了name 參數所能代表的值的種類,也就是name 參數的類型。 本例中委托的定義: // ************************************************************************ public delegate void GreetingDelegate(string name); // ************************************************************************ 可以與上面EnglishGreeting()方法的簽名對比一下,除了加入了delegate 關鍵字以外,其余的是不是完全一樣? 現在,讓我們再次改動GreetPeople()方法,如下所示: // ************************************************************************ public void GreetPeople(string name, GreetingDelegate MakeGreeting) { MakeGreeting(name); } // ************************************************************************ 如你所見,委托GreetingDelegate 出現的位置與 string 相同,string 是一個類型,那么GreetingDelegate 應該也是一個類型,或者叫類(Class)。
但是委托的聲明方式和類卻完全不同,這是怎么一回事?實際上,委托在編譯的時候確實會編譯成類。因為Delegate 是一個類,所以在任何可以聲明類的地方都可以聲明委托。
更多的內容將在下面講述,現在,請看看這個范例的完整代碼:
// ************************************************************************ using System; using System.Collections.Generic; using System.Text; namespace 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); } //注意此方法,它接受一個GreetingDelegate 類型的方法作為參數 private static void GreetPeople(string name, GreetingDelegate MakeGreeting) { MakeGreeting(name); } static void Main(string[] args) { GreetPeople("Liker", EnglishGreeting); GreetPeople("李志中", ChineseGreeting); Console.ReadLine(); } } } // ************************************************************************ 輸出如下: // ************************************************************************ Good Morning, Liker 早上好, 李志中 // ************************************************************************ 我們現在對委托做一個總結:委托是一個類,它定義了方法的類型,使得可以將方法當作另一個方法的參數來進行傳遞,這種將方法動態地賦給參數的做法,
可以避免在程序中大量使用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; // 給此委托變量再綁定一個方法 // 將先后調用 EnglishGreeting 與 ChineseGreeting 方法 GreetPeople("Liker", delegate1); Console.ReadLine(); } // ************************************************************************ 輸出為: // ************************************************************************ Good Morning, Liker 早上好, Liker // ************************************************************************ 實際上,我們可以也可以繞過GreetPeople 方法,通過委托來直接調用EnglishGreeting 和ChineseGreeting: // ************************************************************************ static void Main(string[] args) { GreetingDelegate delegate1; delegate1 = EnglishGreeting; // 先給委托類型的變量賦值 delegate1 += ChineseGreeting; // 給此委托變量再綁定一個方法 // 將先后調用 EnglishGreeting 與 ChineseGreeting 方法 delegate1 ("Liker"); Console.ReadLine(); } // ************************************************************************ 說 明:這在本例中是沒有問題的,但回頭看下上面GreetPeople()的定義,在它之中可以做一些對於EnglshihGreeting 和ChineseGreeting 來說都需要進行的工作,
為了簡便我做了省略。 注意這里,第一次用的“
=”,是賦值的語法;第二次,用的是“+=”,是綁定的語法。如果第一次就使用“+=”,將出現“使用了未賦值的局部變量”的編譯錯誤。
我們也可以使用下面的代碼來這樣簡化這一過程:
// ************************************************************************ GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting); delegate1 += ChineseGreeting; // 給此委托變量再綁定一個方法 // ************************************************************************ 看到這里,應該注意到,這段代碼第一條語句與實例化一個類是何其的相似,你不禁想到:上面第一次綁定委托時不可以使用“+=”的編譯錯誤,或許可以用這樣的方法來避免: // ************************************************************************ GreetingDelegate delegate1 = new GreetingDelegate(); delegate1 += EnglishGreeting; // 這次用的是“+=”,綁定語法。 delegate1 += ChineseGreeting; // 給此委托變量再綁定一個方法 // ************************************************************************ 但實際上,這樣會出現編譯錯誤: “GreetingDelegate”方法沒有采用“0”個參數的重載。盡管這樣的結果讓我們覺得有點沮喪,
但是編譯的提示:“沒有0 個參數的重載”再次讓我們聯想到了類的構造函數。我知道你一定按捺不住想探個究竟,但再此之前,我們需要先把基礎知識和應用介紹完。 既然給委托可以綁定一個方法,那么也應該有辦法取消對方法的綁定,很容易想到,這個語法是“
-=”: // ************************************************************************ static void Main(string[] args) { GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting); delegate1 += ChineseGreeting; // 給此委托變量再綁定一個方法 // 將先后調用 EnglishGreeting 與 ChineseGreeting 方法 GreetPeople("Liker", delegate1); Console.WriteLine(); delegate1 -= EnglishGreeting; //取消對EnglishGreeting 方法的綁定 // 將僅調用 ChineseGreeting GreetPeople("李志中", delegate1); Console.ReadLine(); } // ************************************************************************ 輸出為: // ************************************************************************ Good Morning, Liker 早上好, Liker 早上好, 李志中 // ************************************************************************ 讓我們再次對委托作個總結:使用委托可以將多個方法綁定到同一個委托變量,當調用此變量時(這里用“調用”這個詞,是因為此變量代表一個方法),可以依次調用所有綁定的方法。 1.2 事件的由來 1.2.1 更好的封裝性 我們繼續思考上面的程序:上面的三個方法都定義在 Programe 類中,這樣做是為了理解的方便,實際應用中,通常都是 GreetPeople 在一個類中,
ChineseGreeting 和 EnglishGreeting 在另外的類中。現在你已經對委托有了初步了解,是時候對上面的例子做個改進了。
假設我們將GreetingPeople()放在一個叫GreetingManager 的類中,那么新程序應該是這個樣子的:
// ************************************************************************ 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("Good Morning, " + name); } private static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } static void Main(string[] args) { // ... ... } } } // ************************************************************************ 這個時候,如果要實現前面演示的輸出效果,Main 方法我想應該是這樣的: // ************************************************************************ 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 { //在GreetingManager 類的內部聲明delegate1 變量 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 字段: // ************************************************************************ gm.GreetPeople("Liker", gm.delegate1); // ************************************************************************ 既然如此,我們何不修改 GreetingManager 類成這樣: // ************************************************************************ public class GreetingManager { //在GreetingManager 類的內部聲明delegate1 變量 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 變量 } // ************************************************************************ 輸出為: // ************************************************************************ Good Morning, Liker 早上好, Liker // ************************************************************************ 盡管這樣達到了我們要的效果,但是還是存在着問題:在這里,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"); } // ************************************************************************ 會得到編譯錯誤:事件“Delegate.GreetingManager.MakeGreet”只能出現在“+=”或者“-=”的左邊(從類型“Delegate.GreetingManager”中使用時除外)。 1.2.2 限制類型能力 使用事件不僅能獲得比委托更好的封裝性以外,還能限制含有事件的類型的能力。這是什么意思呢?它的意思是說:事件應該由事件發布者觸發,
而不應該由事件的客戶端(客戶程序)來觸發。請看下面的范例:
// ************************************************************************ 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); // 但可以被這樣直接調用,對委托變量的不恰當使用 } } // 定義委托 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); } } // ************************************************************************ 上面代碼定義了一個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 的聲明語句做一探究,看看為什么會發生這樣的錯誤: // ************************************************************************ public event GreetingDelegate MakeGreet; // ************************************************************************ 可以看到,實際上盡管我們在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); } // ************************************************************************ 1.4 .NET 框架中的委托和事件 1.4.1 范例說明 上面的例子已不足以再進行下面的講解了,我們來看一個新的范例,因為之前已經介紹了很多的內容,所以本節的進度會稍微快一些: 假設我們有個高檔的熱水器,我們給它通上電,當水溫超過95 度的時候:1、揚聲器會開始發出語音,告訴你水的溫度;2、液晶屏也會改變水溫的顯示,來提示水已經快燒開了。 現在我們需要寫個程序來模擬這個燒水的過程,我們將定義一個類來代表熱水器,我們管它叫:Heater,它有代表水溫的字段,叫做temperature;
當然,還有必不可少的給水加熱方法BoilWater(),一個發出語音警報的方法MakeAlert(),一個顯示水溫的方法,ShowMsg()。
// ************************************************************************ namespace Delegate { public class Heater { private int temperature; // 水溫 // 燒水 public void BoilWater() { for (int i = 0; i <= 100; i++) { temperature = i; if (temperature > 95) { MakeAlert(temperature); ShowMsg(temperature); } } } // 發出語音警報 private void MakeAlert(int param) { Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , 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 設計模式簡介 上面的例子顯然能完成我們之前描述的工作,但是卻並不夠好。現在假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自於不同廠商並進行了組裝。
那么,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。 這時候,上面的例子就應該變成這個樣子:
// ************************************************************************ // 熱水器 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:監視對象,它往往包含着其他對象所感興趣的內容。在本范例中,熱水器就是一個監視對象,它包含的其他對象所感興趣的內容,就是temprature 字段,
當這個字段的值快到100 時,會不斷把數據發給監視它的對象。 Observer:監視者,它監視Subject,當Subject 中的某件事發生的時候,會告知Observer,而Observer 則會采取相應的行動。
在本范例中,Observer 有警報器和顯示器,它們采取的行動分別是發出警報和顯示水溫。 在本例中,事情發生的順序應該是這樣的:
1. 警報器和顯示器告訴熱水器,它對它的溫度比較感興趣(注冊)。 2. 熱水器知道后保留對警報器和顯示器的引用。 3. 熱水器進行燒水這一動作,當水溫超過 95 度時,通過對警報器和顯示器的引用,自動調用警報器的MakeAlert()方法、顯示器的ShowMsg()方法。 類似這樣的例子是很多的,GOF 對它進行了抽象,稱為Observer 設計模式:Observer 設計模式是為了定義對象間的一種一對多的依賴關系,以便於當一個對象的狀態改變時,
其他依賴於它的對象會被自動告知並更新。Observer 模式是一種松耦合的設計模式。
1.4.3 實現范例的Observer 設計模式 我們之前已經對委托和事件介紹很多了,現在寫代碼應該很容易了,現在在這里直接給出代碼,並在注釋中加以說明。 // ************************************************************************ using System; using System.Collections.Generic; using System.Text; 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,為什么呢?這是因為委托變量可以供多個訂閱者注冊,如果定義了返回值,
那么多個訂閱者的方法都會向發布者返回數值,結果就是后面一個返回的方法值將前面的返回值覆蓋掉了,因此,實際上只能獲得最后一個方法調用的返回值。
可以運行下面的代碼測試一下。除此以外,發布者和訂閱者是松耦合的,發布者根本不關心誰訂閱了它的事件、為什么要訂閱,更別說訂閱者的返回值了,
所以返回訂閱者的方法返回值大多數情況下根本沒有必要。
// ************************************************************************ class Program { static void Main(string[] args) { Publishser pub = new Publishser(); Subscriber1 sub1 = new Subscriber1(); Subscriber2 sub2 = new Subscriber2(); Subscriber3 sub3 = new Subscriber3(); pub.NumberChanged += new GeneralEventHandler(sub1.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(sub2.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(sub3.OnNumberChanged); pub.DoSomething(); // 觸發事件 } } // 定義委托 public delegate string GeneralEventHandler(); // 定義事件發布者 public class Publishser { public event GeneralEventHandler NumberChanged; // 聲明一個事件 public void DoSomething() { if (NumberChanged != null) { // 觸發事件 string rtn = NumberChanged(); Console.WriteLine(rtn); // 打印返回的字符串,輸出為Subscriber3 } } } // 定義事件訂閱者 public class Subscriber1 { public string OnNumberChanged() { return "Subscriber1"; } } public class Subscriber2 { /* 略,與上類似,返回Subscriber2*/ } public class Subscriber3 { /* 略,與上類似,返回Subscriber3*/ } // ************************************************************************ 如果運行這段代碼,得到的輸出是Subscriber3,可以看到,只得到了最后一個注冊方法的返回值。 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、將NumberChanged 聲明為委托變量還是事件都無所謂了,因為它是私有的,即便將它聲明為一個委托變量,客戶端也看不到它,也就無法通過它來觸發事件、
調用訂閱者的方法。而只能通過Register()和UnRegister()方法來注冊和取消注冊,通過調用DoSomething()方法觸發事件(而不是NumberChanged 本身,
這在前面已經討論過了)。
2、我們還應該發現,這里采用的、對NumberChanged 委托變量的訪問模式和C#中的屬性是多么類似啊?大家知道,在C#中通常一個屬性對應一個類型成員,
而在類型的外部對成員的操作全部通過屬性來完成。盡管這里對委托變量的處理是類似的效果,但卻使用了兩個方法來進行模擬,有沒有辦法像使用屬性一樣來完成上面的例子呢?
答案是有的,C#中提供了一種叫事件訪問器(Event Accessor)的東西,它用來封裝委托變量。如下面例子所示:
// ************************************************************************ class Program { static void Main(string[] args) { Publishser pub = new Publishser(); Subscriber1 sub1 = new Subscriber1(); Subscriber2 sub2 = new Subscriber2(); pub.NumberChanged -= sub1.OnNumberChanged; // 不會有任何反應 pub.NumberChanged += sub2.OnNumberChanged; // 注冊了sub2 pub.NumberChanged += sub1.OnNumberChanged; // sub1 將sub2 的覆蓋掉了 pub.DoSomething(); // 觸發事件 } } // 定義委托 public delegate string GeneralEventHandler(); // 定義事件發布者 public class Publishser { // 聲明一個委托變量 private GeneralEventHandler numberChanged; // 事件訪問器的定義 public event GeneralEventHandler NumberChanged { add { numberChanged = value; } remove { numberChanged -= value; } } public void DoSomething() { // 做某些其他的事情 if (numberChanged != null) { // 通過委托變量觸發事件 string rtn = numberChanged(); Console.WriteLine("Return: {0}", rtn); // 打印返回的字符串 } } } // 定義事件訂閱者 public class Subscriber1 { public string OnNumberChanged() { Console.WriteLine("Subscriber1 Invoked!"); return "Subscriber1"; } } public class Subscriber2 {/* 與上類同,略 */} public class Subscriber3 {/* 與上類同,略 */} // ************************************************************************ 上面代碼中類似屬性的public event GeneralEventHandler NumberChanged {add{...}remove{...}}語句便是事件訪問器。使用了事件訪問器以后,
在DoSomething 方法中便只能通過numberChanged 委托變量來觸發事件,而不能NumberChanged 事件訪問器(注意它們的大小寫不同)觸發,它只用於注冊和取消注冊。
下面是代碼輸出:
// ************************************************************************ Subscriber1 Invoked! Return: Subscriber1 // ************************************************************************ 1.5.3 獲得多個返回值與異常處理 現在假設我們想要獲得多個訂閱者的返回值,以List<string>的形式返回,該如何做呢?我們應該記得委托定義在編譯時會生成一個繼承自MulticastDelegate 的類,
而這個MulticastDelegate又繼承自Delegate,在Delegate 內部,維護了一個委托鏈表,鏈表上的每一個元素,為一個只包含一個目標方法的委托對象。
而通過Delegate 基類的GetInvocationList()靜態方法,可以獲得這個委托鏈表。隨后我們遍歷這個鏈表,通過鏈表中的每個委托對象來調用方法,
這樣就可以分別獲得每個方法的返回值:
// ************************************************************************ class Program4 { static void Main(string[] args) { Publishser pub = new Publishser(); Subscriber1 sub1 = new Subscriber1(); Subscriber2 sub2 = new Subscriber2(); Subscriber3 sub3 = new Subscriber3(); pub.NumberChanged += new DemoEventHandler(sub1.OnNumberChanged); pub.NumberChanged += new DemoEventHandler(sub2.OnNumberChanged); pub.NumberChanged += new DemoEventHandler(sub3.OnNumberChanged); List<string> list = pub.DoSomething(); //調用方法,在方法內觸發事件 foreach (string str in list) { Console.WriteLine(str); } } } public delegate string DemoEventHandler(int num); // 定義事件發布者 public class Publishser { public event DemoEventHandler NumberChanged; // 聲明一個事件 public List<string> DoSomething() { // 做某些其他的事 List<string> strList = new List<string>(); if (NumberChanged == null) return strList; // 獲得委托數組 Delegate[] delArray = NumberChanged.GetInvocationList(); foreach (Delegate del in delArray) { // 進行一個向下轉換 DemoEventHandler method = (DemoEventHandler)del; strList.Add(method(100)); // 調用方法並獲取返回值 } return strList; } } // 定義事件訂閱者 public class Subscriber1 { public string OnNumberChanged(int num) { Console.WriteLine("Subscriber1 invoked, number:{0}", num); return "[Subscriber1 returned]"; } } public class Subscriber3 {與上面類同,略} public class Subscriber3 {與上面類同,略} // ************************************************************************ 如果運行上面的代碼,可以得到這樣的輸出: // ************************************************************************ Subscriber1 invoked, number:100 Subscriber2 invoked, number:100 Subscriber3 invoked, number:100 [Subscriber1 returned] [Subscriber2 returned] [Subscriber3 returned] // ************************************************************************ 可見我們獲得了三個方法的返回值。而我們前面說過,很多情況下委托的定義都不包含返回值,所以上面介紹的方法似乎沒有什么實際意義。
其實通過這種方式來觸發事件最常見的情況應該是在異常處理中,因為很有可能在觸發事件時,訂閱者的方法會拋出異常,而這一異常會直接影響到發布者,
使得發布者程序中止,而后面訂閱者的方法將不會被執行。因此我們需要加上異常處理,考慮下面一段程序:
// ************************************************************************ class Program5 { static void Main(string[] args) { Publisher pub = new Publisher(); Subscriber1 sub1 = new Subscriber1(); Subscriber2 sub2 = new Subscriber2(); Subscriber3 sub3 = new Subscriber3(); pub.NumberChanged += new DemoEventHandler(sub1.OnNumberChanged); pub.NumberChanged += new DemoEventHandler(sub2.OnNumberChanged); pub.NumberChanged += new DemoEventHandler(sub3.OnNumberChanged); } } public class Publisher { public event EventHandler MyEvent; public void DoSomething() { // 做某些其他的事情 if (MyEvent != null) { try { MyEvent(this, EventArgs.Empty); } catch (Exception e) { Console.WriteLine("Exception: {0}", e.Message); } } } } public class Subscriber1 { public void OnEvent(object sender, EventArgs e) { Console.WriteLine("Subscriber1 Invoked!"); } } public class Subscriber2 { public void OnEvent(object sender, EventArgs e) { throw new Exception("Subscriber2 Failed"); } } public class Subscriber3 {/* 與Subsciber1 類同,略*/} // ************************************************************************ 注意到我們在Subscriber2 中拋出了異常,同時我們在Publisher 中使用了try/catch 語句來處理異常。運行上面的代碼,我們得到的結果是: // ************************************************************************ Subscriber1 Invoked! Exception: Subscriber2 Failed // ************************************************************************ 可以看到,盡管我們捕獲了異常,使得程序沒有異常結束,但是卻影響到了后面的訂閱者,因為Subscriber3 也訂閱了事件,但是卻沒有收到事件通知(它的方法沒有被調用)。
此時,我們可以采用上面的辦法,先獲得委托鏈表,然后在遍歷鏈表的循環中處理異常,我們只需要修改一下DoSomething 方法就可以了:
// ************************************************************************ public void DoSomething() { if (MyEvent != null) { Delegate[] delArray = MyEvent.GetInvocationList(); foreach (Delegate del in delArray) { EventHandler method = (EventHandler)del; // 強制轉換為具體的委托類型 try { method(this, EventArgs.Empty); } catch (Exception e) { Console.WriteLine("Exception: {0}", e.Message); } } } } // ************************************************************************ 注意到Delegate 是EventHandler 的基類,所以為了觸發事件,先要進行一個向下的強制轉換,之后才能在其上觸發事件,調用所有注冊對象的方法。除了使用這種方式以外,
還有一種更靈活方式可以調用方法,它是定義在Delegate 基類中的DynamicInvoke()方法:
// ************************************************************************ public object DynamicInvoke(params object[] args); // ************************************************************************ 這可能是調用委托最通用的方法了,適用於所有類型的委托。它接受的參數為object[],也就是說它可以將任意數量的任意類型作為參數,並返回單個object 對象。
上面的DoSomething()方法也可以改寫成下面這種通用形式:
// ************************************************************************ public void DoSomething() { // 做某些其他的事情 if (MyEvent != null) { Delegate[] delArray = MyEvent.GetInvocationList(); foreach (Delegate del in delArray) { try { // 使用DynamicInvoke 方法觸發事件 del.DynamicInvoke(this, EventArgs.Empty); } catch (Exception e) { Console.WriteLine("Exception: {0}", e.Message); } } } } // ************************************************************************ 注意現在在DoSomething()方法中,我們取消了向具體委托類型的向下轉換,現在沒有了任何的基於特定委托類型的代碼,而DynamicInvoke 又可以接受任何類型的參數,
且返回一個object對象。所以我們完全可以將DoSomething()方法抽象出來,使它成為一個公共方法,然后供其他類來調用,我們將這個方法聲明為靜態的,
然后定義在Program 類中:
// ************************************************************************ // 觸發某個事件,以列表形式返回所有方法的返回值 public static object[] FireEvent(Delegate del, params object[] args){ List<object> objList = new List<object>(); if (del != null) { Delegate[] delArray = del.GetInvocationList(); foreach (Delegate method in delArray) { try { // 使用DynamicInvoke 方法觸發事件 object obj = method.DynamicInvoke(args); if (obj != null) objList.Add(obj); } catch { } } } return objList.ToArray(); } // ************************************************************************ 隨后,我們在DoSomething()中只要簡單的調用一下這個方法就可以了: // ************************************************************************ public void DoSomething() { // 做某些其他的事情 Program5.FireEvent(MyEvent, this, EventArgs.Empty); } // ************************************************************************ 注意FireEvent()方法還可以返回一個object[]數組,這個數組包括了所有訂閱者方法的返回值。而在上面的例子中,我沒有演示如何獲取並使用這個數組,
為了節省篇幅,這里也不再贅述了,在本書附帶的代碼中,有關於這部分的演示,有興趣的朋友可以下載下來看看。
1.6 訂閱者方法超時的處理 訂閱者除了可以通過異常的方式來影響發布者以外,還可以通過另一種方式:超時。一般說超時,指的是方法的執行超過某個指定的時間,而這里我將含義擴展了一下,
凡是方法執行的時間比較長,我就認為它超時了,這個“比較長”是一個比較模糊的概念,
2 秒、3 秒、5 秒都可以視為超時。
超時和異常的區別就是超時並不會影響事件的正確觸發和程序的正常運行,卻會導致事件觸發后需要很長才能夠結束。在依次執行訂閱者的方法這段期間內,客戶端程序會被中斷,
什么也不能做。因為當執行訂閱者方法時(通過委托,相當於依次調用所有注冊了的方法),當前線程會轉去執行方法中的代碼,調用方法的客戶端會被中斷,
只有當方法執行完畢並返回時,控制權才會回到客戶端,從而繼續執行下面的代碼。我們來看一下下面一個例子:
// ************************************************************************ class Program6 { static void Main(string[] args) { Publisher pub = new Publisher(); Subscriber1 sub1 = new Subscriber1(); Subscriber2 sub2 = new Subscriber2(); Subscriber3 sub3 = new Subscriber3(); pub.MyEvent += new EventHandler(sub1.OnEvent); pub.MyEvent += new EventHandler(sub2.OnEvent); pub.MyEvent += new EventHandler(sub3.OnEvent); pub.DoSomething(); // 觸發事件 Console.WriteLine("\nControl back to client!"); // 返回控制權 } // 觸發某個事件,以列表形式返回所有方法的返回值 public static object[] FireEvent(Delegate del, params object[] args) { // 代碼與上同,略 } } public class Publisher { public event EventHandler MyEvent; public void DoSomething() { // 做某些其他的事情 Console.WriteLine("DoSomething invoked!"); Program6.FireEvent(MyEvent, this, EventArgs.Empty); //觸發事件 } } public class Subscriber1 { public void OnEvent(object sender, EventArgs e) { Thread.Sleep(TimeSpan.FromSeconds(3)); Console.WriteLine("Waited for 3 seconds, subscriber1 invoked!"); } } public class Subscriber2 { public void OnEvent(object sender, EventArgs e) { Console.WriteLine("Subscriber2 immediately Invoked!"); } } public class Subscriber3 { public void OnEvent(object sender, EventArgs e) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("Waited for 2 seconds, subscriber2 invoked!"); } } // ************************************************************************ 在這段代碼中, 我們使用Thread.Sleep() 靜態方法模擬了方法超時的情況。其中Subscriber1.OnEvent()需要三秒鍾完成,Subscriber2.OnEvent()立即執行,
Subscriber3.OnEvent 需要兩秒完成。這段代碼完全可以正常輸出,也沒有異常拋出(如果有,也僅僅是該訂閱者被忽略掉), 下面是輸出的情況:
// ************************************************************************ DoSomething invoked! Waited for 3 seconds, subscriber1 invoked! Subscriber2 immediately Invoked! Waited for 2 seconds, subscriber2 invoked! Control back to client! // ************************************************************************ 但是這段程序在調用方法DoSomething()、打印了“DoSomething invoked”之后,觸發了事件,隨后必須等訂閱者的三個方法全部執行完畢了之后,也就是大概5 秒鍾的時間,
才能繼續執行下面的語句,也就是打印“Control back to client”。而我們前面說過,很多情況下,尤其是遠程調用的時候(比如說在Remoting 中),
發布者和訂閱者應該是完全的松耦合,發布者不關心誰訂閱了它、不關心訂閱者的方法有什么返回值、不關心訂閱者會不會拋出異常,
當然也不關心訂閱者需要多長時間才能完成訂閱的方法,它只要在事件發生的那一瞬間告知訂閱者事件已經發生並將相關參數傳給訂閱者就可以了。
然后它就應該繼續執行它后面的動作,在本例中就是打印“Control back to client!”。而訂閱者不管失敗或是超時都不應該影響到發布者,但在上面的例子中,
發布者卻不得不等待訂閱者的方法執行完畢才能繼續運行。 現在我們來看下如何解決這個問題,先回顧一下之前我在C#中的委托和事件一文中提到的內容,我說過,委托的定義會生成繼承自MulticastDelegate 的完整的類,
其中包含Invoke()、BeginInvoke()和EndInvoke()方法。當我們直接調用委托時,實際上是調用了Invoke()方法,它會中斷調用它的客戶端,
然后在客戶端線程上執行所有訂閱者的方法(客戶端無法繼續執行后面代碼),最后將控制權返回客戶端。注意到BeginInvoke()、EndInvoke()方法,
在.NET 中,異步執行的方法通常都會配對出現,並且以Begin 和End 作為方法的開頭(最常見的可能就是Stream 類的BeginRead()和EndRead()方法了)。
它們用於方法的異步執行,即是在調用BeginInvoke()之后,客戶端從線程池中抓取一個閑置線程,然后交由這個線程去執行訂閱者的方法,
而客戶端線程則可以繼續執行下面的代碼。 BeginInvoke()接受“動態”的參數個數和類型,為什么說“動態”的呢?因為它的參數是在編譯時根據委托的定義動態生成的,
其中前面參數的個數和類型與委托定義中接受的參數個數和類型相同,最后兩個參數分別是AsyncCallback 和Object 類型,對於它們更具體的內容,
可以參見下一節委托和方法的異步調用部分。現在,我們僅需要對這兩個參數傳入null 就可以了。另外還需要注意幾點:
1. 在委托類型上調用 BeginInvoke()時,此委托對象只能包含一個目標方法,所以對於多個訂閱者注冊的情況,必須使用GetInvocationList()獲得所有委托對象,
然后遍歷它們,分別在其上 調用BeginInvoke()方法。如果直接在委托上調用BeginInvoke(),會拋出異常,提示“委托只能包含一個目標方法”。
2. 如果訂閱者的方法拋出異常,.NET 會捕捉到它,但是只有在調用EndInvoke()的時候,才會將異常重新拋出。而在本例中,
我們不使用EndInvoke(() 因為我們不關心訂閱者的執行情況),所以我們無需處理異常,因為即使拋出異常,也是在另一個線程上,
不會影響到客戶端線程(客戶端甚至不知道訂閱者發生了異常,這有時是好事有時是壞事)。
3. BeginInvoke()方法屬於委托定義所生成的類,它既不屬於MulticastDelegate 也不屬於Delegate基類,所以無法繼續使用可重用的FireEvent()方法,
我們需要進行一個向下轉換,來獲取到實際的委托類型。 現在我們修改一下上面的程序,使用異步調用來解決訂閱者方法執行超時的情況:
// ************************************************************************ class Program6 { static void Main(string[] args) { Publisher pub = new Publisher(); Subscriber1 sub1 = new Subscriber1(); Subscriber2 sub2 = new Subscriber2(); Subscriber3 sub3 = new Subscriber3(); pub.MyEvent += new EventHandler(sub1.OnEvent); pub.MyEvent += new EventHandler(sub2.OnEvent); pub.MyEvent += new EventHandler(sub3.OnEvent); pub.DoSomething(); // 觸發事件 Console.WriteLine("Control back to client!\n"); // 返回控制權 Console.WriteLine("Press any thing to exit..."); Console.ReadLine(); // 暫停客戶程序,提供時間供訂閱者完成方法 } } public class Publisher { public event EventHandler MyEvent; public void DoSomething() { // 做某些其他的事情 Console.WriteLine("DoSomething invoked!"); if (MyEvent != null) { Delegate[] delArray = MyEvent.GetInvocationList(); foreach (Delegate del in delArray) { EventHandler method = (EventHandler)del; method.BeginInvoke(null, EventArgs.Empty, null, null); } } } } public class Subscriber1 { public void OnEvent(object sender, EventArgs e) { Thread.Sleep(TimeSpan.FromSeconds(3)); // 模擬耗時三秒才能完成方法 Console.WriteLine("Waited for 3 seconds, subscriber1 invoked!"); } } public class Subscriber2 { public void OnEvent(object sender, EventArgs e) { throw new Exception("Subsciber2 Failed"); // 即使拋出異常也不會影響到客戶端 //Console.WriteLine("Subscriber2 immediately Invoked!"); } } public class Subscriber3 { public void OnEvent(object sender, EventArgs e) { Thread.Sleep(TimeSpan.FromSeconds(2)); // 模擬耗時兩秒才能完成方法 Console.WriteLine("Waited for 2 seconds, subscriber3 invoked!"); } } // ************************************************************************ 運行上面的代碼,會得到下面的輸出: // ************************************************************************ DoSomething invoked! Control back to client! Press any thing to exit... Waited for 2 seconds, subscriber3 invoked! Waited for 3 seconds, subscriber1 invoked! // ************************************************************************ 需要注意代碼輸出中的幾個變化: 1. 我們需要在客戶端程序中調用 Console.ReadLine()方法來暫停客戶端,以提供足夠的時間來讓異步方法去執行完代碼,不然的話客戶端的程序到此處便會運行結束,
程序會退出,不會看到任何訂閱者方法的輸出,因為它們根本沒來得及執行完畢。原因是這樣的:客戶端所在的線程我們通常稱為主線程,而執行訂閱者方法的線程來自線程池,
屬於后台線程(Background Thread),當主線程結束時,不論后台線程有沒有結束,都會退出程序。(當然還有一種前台線程(Foreground Thread),
主線程結束后必須等前台線程也結束后程序才會退出,關於線程的討論可以開辟另一個龐大的主題,這里就不討論了)。
2. 在打印完“Press any thing to exit...”之后,兩個訂閱者的方法會以2 秒、1 秒的間隔顯示出來,且盡管我們先注冊了subscirber1,
但是卻先執行了subscriber3,這是因為執行它需要的時間更短。除此以外,注意到這兩個方法是並行執行的,所以執行它們的總時間是最長的方法所需要的時間,
也就是3 秒,而不是他們的累加5 秒。
3. 如同前面所提到的,盡管 subscriber2 拋出了異常,我們也沒有針對異常進行處理,但是客戶程序並沒有察覺到,程序也沒有因此而中斷。 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(),
或者是根據情況在晚些時候調用。說了這么多,我們現在看一下使用異步調用改寫后上面的代碼吧:
// ************************************************************************ 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 { /* 與上面同,略 */} // ************************************************************************ 此時的輸出為: // ************************************************************************ Client application started! Method invoked! Main Thread: Client executed 1 second(s). Pool Thread: Add executed 1 second(s). Main Thread: Client executed 2 second(s). Pool Thread: Add executed 2 second(s). Method complete! Main Thread: Client executed 3 second(s). Result: 7 Press any key to exit... // ************************************************************************ 現在執行完這段代碼只需要3 秒鍾時間,兩個for 循環所產生的輸出交替進行,這也說明了這兩段代碼並行執行的情況。可以看到Add() 方法是由線程池中的線程在執行,
因為Thread.CurrentThread.IsThreadPoolThread 返回了True,同時我們對該線程命名為了Pool Thread。 另外我們可以看到通過EndInvoke()方法得到了返回值。有時候,我們可能會將獲得返回值的操作放到另一段代碼或者客戶端去執行,
而不是向上面那樣直接寫在BeginInvoke()的后面。比如說我們在Program 中新建一個方法GetReturn(),此時可以通過AsyncResult 的AsyncDelegate
獲得del 委托對象,然后再在其上調用EndInvoke()方法,這也說明了AsyncResult 可以唯一的獲取到與它相關的調用了的方法(或者也可以理解成委托對象)。
所以上面獲取返回值的代碼也可以改寫成這樣:
// ************************************************************************ static int GetReturn(IAsyncResult asyncResult) { AsyncResult result = (AsyncResult)asyncResult; AddDelegate del = (AddDelegate)result.AsyncDelegate; int rtn = del.EndInvoke(asyncResult); return rtn; } // ************************************************************************ 然后再將int rtn = del.EndInvoke(asyncResult);語句改為int rtn = GetReturn(asyncResult);。注意上面IAsyncResult 要轉換為實際的類型AsyncResult
才能訪問AsyncDelegate 屬性,因為它沒有包含在IAsyncResult 接口的定義中。 BeginInvoke 的另外兩個參數分別是AsyncCallback 和Object 類型,其中AsyncCallback 是一個委托類型,它用於方法的回調,
即是說當異步方法執行完畢時自動進行調用的方法。它的定義為:
// ************************************************************************ public delegate void AsyncCallback(IAsyncResult ar); // ************************************************************************ Object 類型用於傳遞任何你想要的數值,它可以通過IAsyncResult 的AsyncState 屬性獲得。下面我們將獲取方法返回值、
打印返回值的操作放到了OnAddComplete()回調方法中:
// ************************************************************************ public delegate int AddDelegate(int x, int y); class Program9 { 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); string data = "Any data you want to pass."; AsyncCallback callBack = new AsyncCallback(OnAddComplete); del.BeginInvoke(2, 5, callBack, data); // 異步調用方法 // 做某些其它的事情,模擬需要執行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(); } static void OnAddComplete(IAsyncResult asyncResult) { AsyncResult result = (AsyncResult)asyncResult; AddDelegate del = (AddDelegate)result.AsyncDelegate; string data = (string)asyncResult.AsyncState; int rtn = del.EndInvoke(asyncResult); Console.WriteLine("{0}: Result, {1}; Data: {2}\n", Thread.CurrentThread.Name, rtn, data); } } public class Calculator { /* 與上面同,略 */} // ************************************************************************ 它產生的輸出為: // ************************************************************************ Client application started! Method invoked! Main Thread: Client executed 1 second(s). Pool Thread: Add executed 1 second(s). Main Thread: Client executed 2 second(s). Pool Thread: Add executed 2 second(s). Method complete! Pool Thread: Result, 7; Data: Any data you want to pass. Main Thread: Client executed 3 second(s). Press any key to exit... // ************************************************************************ 這里有幾個值得注意的地方:
1、我們在調用BeginInvoke()后不再需要保存IAysncResult 了,因為AysncCallback 委托將該對象定義在了回調方法的參數列表中;
2、我們在OnAddComplete()方法中獲得了調用BeginInvoke()時最后一個參數傳遞的值,字符串“Any data you want to pass”;
3、執行回調方法的線程並非客戶端線程Main Thread,而是來自線程池中的線程Pool Thread。另外如前面所說,在調用EndInvoke()時有可能會拋出異常,
所以在應該將它放到try/catch 塊中,這里我就不再示范了。 1.8 總結 在章中,我們詳細地討論了C#中的委托和事件,包括什么是委托、為什么要使用委托、事件的由來、.NET Framework 中的委托和事件、委托中方法異常和超時的處理、
委托與異步編程、委托和事件對Observer 設計模式的意義。擁有了本章的知識,相信你以后遇到委托和事件時,將不會再有所畏懼。

 


免責聲明!

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



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