事件
發布者和訂閱者
很多程序都有一個共同的需求,既當一個特定的程序事件發生時,程序的其他部分可以得到該事件已經發生的通知。
發布者/訂閱者模式(publisher/subscriber pattern)可以滿足這種需求。
- 發布者(publisher) 發布某個事件的類或結構,其他類可以在該事件發生時得到通知
- 訂閱者(subscriber) 注冊並在事件發生時得到通知的類或結構
- 事件處理程序(event handler) 由訂閱者注冊到事件的方法,在發布者觸發事件時執行。事件處理程序可定義在事件所在的類或結構中,也可定義在不同的類或結構中
- 觸發(raise)事件 調用(invoke)或觸發(fire)事件的術語。當事件觸發時,所有注冊到它的方法都會被依次調用
前一章介紹了委托。事件的很多部分都與委托類似。實際上,事件就像專門用於特殊用途的簡單委托。事件包含了一個私有的委托。
有關事件的私有委托需要了解的重要事項如下:
- 事件提供了對它的私有控制委托的結構化訪問。即,你無法直接訪問委托
- 事件中可用的操作比委托少,對於事件我們只可添加、刪除或調用事件處理程序
- 事件被觸發時,它調用委托來依次調用調用列表中的方法
源代碼組件概覽
需要在事件中使用的代碼有5部分。
- 委托類型聲明 事件和事件處理程序必須有共同的簽名和返回類型,它們通過委托類型進行描述
- 事件處理程序聲明 訂閱者類中會在事件觸發時執行的方法聲明。它們不一定是有顯式命名的方法,還可以是第13章描述的匿名方法或Lambda表達式
- 事件聲明 發布者類必須聲明一個訂閱者類可以注冊的事件成員。當聲明的事件為public時,稱為發布了事件
- 事件注冊 訂閱者必須訂閱事件才能在它被觸發時得到通知
- 觸發事件的代碼 發布者類中“觸發”事件並導致調用注冊的所有事件處理程序的代碼
聲明事件
發布者類必須提供事件對象。創建事件比較簡單–只需要委托類型和名字。事件聲明的語法如下代碼所示。
代碼中聲明了CountADozen事件。
- 事件聲明在一個類中
- 它需要委托類型的名稱,任何附加到事件(如注冊)的處理程序都必須與委托類型的簽名和返回類型匹配
- 它聲明為public,這樣其他類和結構可以在它上面注冊事件處理程序
- 不能使用對象創建表達式(new 表達式)來創建對象
class Incrementer { 關鍵字 委托類型 事件名 ↓ ↓ ↓ public event EventHandler CountedADozen; ... }
可以通過使用逗號分隔同時聲明多個事件。
public event EventHandler MyEvent1,MyEvent2,OtherEvent;
還可以使用static關鍵字讓事件變成靜態
public static event EventHandler CountedADozen;
事件是成員
一個常見誤解是把事件認為是類型。和方法、屬性一樣,事件是類或結構的成員,這一點引出幾個重要特性。
- 由於事件是成員:
- 我們不能在一段可執行代碼中聲明事件
- 它必須聲明在類或結構中
- 事件成員被隱式自動初始化為null
事件聲明需要委托類型的名字,我們可以聲明一個委托類型或使用已存在的。如果我們聲明一個委托類型,它必須指定事件保存的方法的簽名和返回類型。
BCL(Base Class Library,基類庫)聲明了一個叫做EventHandler的委托,專門用於系統事件。
訂閱事件
- 使用+=運算符來為事件增加事件處理程序
- 事件處理程序的規范可以是以下任意一種
- 實例方法的名稱
- 靜態方法的名稱
- 匿名方法
- Lambda表達式
例:為CountedADozen事件增加3個方法。
incrementer.CountedADozen+=IncrementDozensCount;//實例方法 incrementer.CountedADozen+=ClassB.CounterHandlerB;//靜態方法 mc.CountedADozen+=new EventHandler(cc.CounterHandlerC);//委托形式
例:Lambda表達式和匿名方法
incrementer.CountedADozen+=()=>DozensCount++;//Lambda表達式 incrementer.CountedADozen+=delegate{DozensCount++;};//匿名方法
觸發事件
事件成員本身只保存了需要被調用的事件處理程序。如果事件沒觸發,什么都不會發生。我們需要確保在合適的時候有代碼來做這件事情。
例:下面代碼觸發了CountedADozen事件。
- 在觸發事件前和null比較,從而查看是否包含事件處理程序,如果事件是null,則表示沒有,不能執行
- 觸發事件的語法和調用方法一樣
- 使用事件名稱,后面跟參數列表
- 參數列表需與事件委托類型相匹配
if(CountedADozen!=null) { CountedADozen(source,args); }
把事件聲明和觸發事件的代碼放在一起便有了如下的發布者類聲明。
下面展示了整個程序,代碼需要注意的地方如下:
- 在構造函數中,Dozens類訂閱事件,將IncrementDozensCount作為事件處理程序
- 在Incrementer類的DoCount方法中,每增長12個數就觸發CountedADozen事件
delegate void Handler(); //聲明委托 //發布者 class Incrementer { public event Handler CountedADozen;//創建事件並發布 void DoCount(object source,EventArgs args) { for(int i=1;i<100;i++) { if((i%12==0)&&(CountedADozen!=null)) { CountedADozen(source,args); } } } } //訂閱者 class Dozens { public int DozensCount{get;private set;} public Dozens(Incrementer incrementer) { DozensCount=0; incrementer.CountedADozen+=IncrementDozensCount;//訂閱事件 } void IncrementDozensCount()//聲明事件處理程序 { DozensCount++; } } class Program { static void Main() { var incrementer=new Incrementer(); var dozensCounter=new Dozens(incrementer); incrementer.DoCount(); Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount); } }
標准事件的用法
GUI編程是事件驅動的,即程序運行時,它可以在任何時候被事件打斷,比如鼠標點擊,按下按鍵或系統定時器。在這些情況發生時,程序需要處理事件然后繼續其他事情。
程序事件的異步處理是使用C#事件的絕佳場景。Windows GUI編程廣泛的使用事件,對於事件的使用,.NET框架提供了一個標准模式。事件使用的標准模式根本就是System命名空間聲明的EventHandler委托類型。EventHandler委托類型的聲明代碼如下。
- 第一個參數用來保存觸發事件的對象的引用。由於是object類型,所以可以匹配任何類型的實例
- 第二個參數用來保存狀態信息,指明什么類型適用於該程序
- 返回類型是void
public delegate void EventHandler(object sender,EventArgs e);
EventHandler委托類型的第二個參數是EventArgs類的UI項,它聲明在System命名空間中。既然第二個參數用於傳遞數據,你可能會誤認為EventArgs類的對象應該可以保存一些類型的數據。
- EventArgs設計為不能傳遞任何數據。它用於不需要傳遞數據的事件處理程序–通常會被忽略
- 如果你希望傳遞數據,必須聲明一個派生自EventArgs的類,使用合適的字段來保存需要傳遞的數據
例:Incrementer+EventHandler
- 在聲明中使用系統定義的EventHandler委托替換Handler
- 訂閱者中聲明的事件處理程序簽名必須與事件委托(object、EventArgs參數)的簽名(和返回類型)匹配。對於IncrementDozensCount事件處理程序來說,該方法忽略了正式參數
- 觸發事件的代碼在調用事件時必須使用適當的參數類型的對象
public delegate void EventHandler(object sender,EventArgs e); //發布者 class Incrementer { public event EventHandler CountedADozen;//創建事件並發布 public void DoCount() { for(int i=1;i<100;i++) { if((i%12==0)&&(CountedADozen!=null)) { CountedADozen(this,null); } } } } //訂閱者 class Dozens { public int DozensCount{get;private set;} public Dozens(Incrementer incrementer) { DozensCount=0; incrementer.CountedADozen+=IncrementDozensCount;//訂閱事件 } void IncrementDozensCount(object source,EventArgs e)//聲明事件處理程序 { DozensCount++; } } class Program { static void Main() { var incrementer=new Incrementer(); var dozensCounter=new Dozens(incrementer); incrementer.DoCount(); Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount); } }
通過擴展EventArgs來傳遞數據
為了向EventArgs傳入數據,並且符合標准慣例,我們需要聲明一個派生自EventArgs的自定義類,用於保存需要傳入的數據。類的名稱應該以EventArgs結尾。
例:聲明自定義的EventArgs類,它將字符串存儲在IterationCount字段中。
public class IncrementerEventArgs:EventArgs { public int IterationCount{get;set;} }
除了自定義類外,你還需要一個使用自定義類的委托類型。要獲取該類,可以使用泛型(第17章)版本的委托EventHandler<>。要使用泛型委托,需要做到以下兩點:
- 將自定義類的名稱放在尖括號內
- 在需要使用自定義委托類型的時候使用整個字符串。
例:使用了自定義類和自定義委托的事件示例
public class IncrementerEventArgs:EventArgs { public int IterationCount{get;set;} } //發布者 class Incrementer { 使用自定義類的泛型委托 ↓ public event EventHandler<IncrementerEventArgs> CountedADozen;//創建事件並發布 public void DoCount() { IncrementerEventArgs args=new IncrementerEventArgs(); for(int i=1;i<100;i++) { if((i%12==0)&&(CountedADozen!=null)) { args.IterationCount=i; CountedADozen(this,args); } } } } //訂閱者 class Dozens { public int DozensCount{get;private set;} public Dozens(Incrementer incrementer) { DozensCount=0; incrementer.CountedADozen+=IncrementDozensCount;//訂閱事件 } void IncrementDozensCount(object source,IncrementerEventArgs e)//聲明事件處理程序 { Console.WriteLine("Incremented at iteration: {0} in {1}",e.IterationCount,source.ToString()); DozensCount++; } } class Program { static void Main() { var incrementer=new Incrementer(); var dozensCounter=new Dozens(incrementer); incrementer.DoCount(); Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount); } }
移除事件處理程序
用完事件處理程序后,可以使用-=運算符把事件處理程序從事件中移除。
例:移除事件處理程序示例
class Publisher { public event EventHandler SimpleEvent; public void RaiseTheEvent(){SimpleEvent(this,null);} } class Subscriber { public void MethodA(object o,EventArgs e) { Console.WriteLine("AAA"); } public void MethodB(object o,EventArgs e) { Console.WriteLine("BBB"); } } class Program { static void Main() { var p=new Publisher(); var s=new Subscriber(); p.SimpleEvent+=s.MethodA; p.SimpleEvent+=s.MethodB; p.RaiseTheEvent(); Console.WriteLine("\r\nRemove MethodB"); p.SimpleEvent-=s.MethodB; p.RaiseTheEvent(); } }
如果一個處理程序向事件注冊了多次,那么移除程序時,將只移除列表中該處理程序的最后一個實例。
事件訪問器
之前我提到+=和-=運算符是事件允許的唯一運算符。看到這里我們應該知道,這些運算符有預定義行為。
我們可以修改這些運算符的行為,並且使用它們時可以讓事件執行任何我們希望的自定義代碼。但這是高級主題,此處只做簡單介紹。
要改變這兩個運算符的操作,可以為事件定義事件訪問器。
- 有兩個訪問器:add和remove
- 聲明事件的訪問器看上去和聲明一個屬性差不多
例:具有訪問器的事件聲明。兩個訪問器都有隱式值參數value,它接受實例或靜態方法的引用。
public event EventHandler CountedADozen { add { ... //執行+=運算符的代碼 } remove { ... //執行-=運算符的代碼 } }
聲明事件訪問器后,事件不包含任何內嵌委托對象。我們必須實現自己的機制來存儲和移除事件注冊方法。
事件訪問器表現為void方法,即沒有返回值。