委託和事件在 .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類型的true和int類型的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方法,經過委託來直接調用EnglishGreeting和ChineseGreeting而且與之傳參。如示例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");
}
儘管這樣達到了咱們要的效果,可是仍是存在着問題:
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_MakeGreet和remove_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 設計模式中主要包括以下兩類對象:
在本例中,事情發生的順序應該是這樣的:
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 熱水器燒水運行結果