2012年7月21日,北京因暴雨災害導致勞命傷財。這個事情過去后,“自然災害預警”系統又一次被人們提起,它就是將自然現象前交發送通知給人們,這個過程能很好地解釋C#語言中的事件。在上一節《C#基礎知識梳理系列五:委托與事件(上)》我們主要討論了與委托相關的知識,包括委托的內部實現、委托鏈等。那么事件與委托是什么關系呢?事件又是如何工作的呢?這些將是這節主要討論的內容。
氣象局可以利用移動通信平台向人們的手機以短信的方式發送天氣情況,只要你的手機在開機狀態,在它向周圍尋找基站並注冊的這個過程就是訂閱者對發布者發布的事件的訂閱過程,正常情況下,只要你的號碼不欠費,基站就允許你注冊成功,事實上,即使你的號碼欠費,你的手機也是在基站注冊成功的,只是運營商拒絕向你提供服務,當移動平台有需要發送的預警信息時,就會向所有已經在基站注冊成功的手機發布預警信息數據(如果他願意,當然也可以向欠費手機發送。)。C#編程中的事件跟上面的注冊過程相似,事件也是類型的一個成員,它的運行機制是以委托為基礎的,定義了事件成員的類型可以向注冊該事件的類或對象發出消息通知。引發事件的類被稱為“發布者”,接收事件的類被稱為“客戶”或訂閱者,從發布者到訂閱者被傳送的數據稱為“事件消息”。像上面所說的移動通信平台就是事件的發布者,人們的手機就是事件的訂閱者,預警信息數據就是事件消息。
發布者知道何時引發事件,訂閱者知道如何響應處理事件。一個發布者可以擁有多個訂閱者,此時發布者維護了一個向自己注冊的客戶的列表,一個訂閱者也可以同時向多個發布者進行事件注冊。當事件發生時,所有向該事件注冊的客戶都會接到通知,客戶程序會以委托的回調方法接收它所訂閱的通知。
事件是用關鍵字event來定義的,還可以給事件指定一個訪問修飾符,一般是public,另外,一個委托類型是指定要調用的方法,通常事件的名稱是以event 結尾的。如下:
public class SMS { public event EventHandler SMSEevent; }
.NET Framework建議我們在定義事件的時候應該定義符合.NET Framework准則的事件,這是更規范的事件模式。那么這個准則有什么要求呢?
(1) 如果在引發事件后,事件發出通知的消息是有自定義數據的,那么應該將這些事件消息專門封裝到一個類中進行打包傳送,通常這個類繼承於System.EventArgs。例如:
public class SMSEventArgs : EventArgs { public string ToPhoneNo { get; set; } public string Message { get; set; } }
當然如果沒有事件消息,也可以不定義這個消息包裝器。
(2) 定義一個委托,用來包裝一個回調函數,當事件引發時,可以通過這個委托來發出通知。如下是.NET Framework已經定義好的一個返回類型為void不包含事件數據的委托原型:
public delegate void EventHandler(object sender, EventArgs e);
當然,我們也可以定義自己的包含事件數據的委托,盡管委托的定義允許其有返回值,但事件模型要求必須定義返回類型為void的委托供事件使用。如下:
public delegate void SMSEventHandler(object sender, SMSEventArgs e);
在委托原型中我們發現有一個參數 object sender,這是事件模型要求的,它是引發事件的對象,並且要求是object 類型,之所以要求是object類型,是方便於繼承,因為促使事件引發的對象可能是很多種類型。另外,盡量保證事件數據參數名為e,這樣更方便使用者理解,如:SMSEventArgs e。
.NET Framework還提供了泛型版本的事件委托:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
這樣在使用此委托時,就可以指定任意類型的事件數據包裝器類型。當你想使用一個事件定義在多種場合下傳遞多種事件數據類型的時候,泛型委托很有用。如下:
public event EventHandler<SMSEventArgs> SMSEevent2;
(3)事件設計准則還建議我們在事件所在的類型中定義一個用於引發事件的方法來通知事件的登記對象,這個方法應該是受保護的虛方法,在派生類中可以重寫此方法來控制對事件通知的發出。該方法通常以On開頭,如下:
protected virtual void OnSMSEevent() { EventHandler handler = this.SMSEevent; if (handler != null) { handler(this, null); } } //如果有事件消息數據,則可以如下定義: protected virtual void OnSMSEevent2(SMSEventArgs e) { EventHandler<SMSEventArgs> handler = this.SMSEevent2; if (handler != null) { handler(this, e); } }
這個方法接收一個SMSEventArgs e參數作為事件消息數據,將被發送給已登記的客戶。
在該方法內,我們建議定義一個臨時變量handler來存儲當前事件的引用,然后對其進行判空,以防止在多線程的情況下事件還沒有登錄客戶對象。
(4)在必要的時候,可以引用上述定義的事件。它通常是在一個方法中來調用(3)中所定義的方法OnXXX來引發一個事件,如下:
public void SendSMS(string toPhoneNo, string message) { //發送短信 if (!string.IsNullOrEmpty(toPhoneNo)) { SMSEventArgs args = new SMSEventArgs(); args.ToPhoneNo = toPhoneNo; args.Message = message; OnSMSEevent2(args); } }
有關事件的定義都已經基本完成了,完整的短信處理程序代碼如下:

1 public class SMS 2 { 3 public event EventHandler SMSEevent; 4 public event EventHandler<SMSEventArgs> SMSEevent2; 5 protected virtual void OnSMSEevent() 6 { 7 EventHandler handler = this.SMSEevent; 8 if (handler != null) 9 { 10 handler(this, null); 11 } 12 } 13 protected virtual void OnSMSEevent2(SMSEventArgs e) 14 { 15 EventHandler<SMSEventArgs> handler = this.SMSEevent2; 16 if (handler != null) 17 { 18 handler(this, e); 19 } 20 } 21 22 public void SendSMS(string toPhoneNo, string message) 23 { 24 //發送短信 25 if (!string.IsNullOrEmpty(toPhoneNo)) 26 { 27 SMSEventArgs args = new SMSEventArgs(); 28 args.ToPhoneNo = toPhoneNo; 29 args.Message = message; 30 OnSMSEevent2(args); 31 } 32 } 33 }
接下來我們來定義客戶程序,就是像事件注冊登錄的客戶對象。這個比較簡單,提供一個回調方法,然后把這個方法注冊給事件,這個過程是通過委托來實現的,如下代碼:
public class Code_05_04 { public Code_05_04(SMS sms) { sms.SMSEevent += new EventHandler(sms_SMSEevent); sms.SMSEevent2 += new EventHandler<SMSEventArgs>(sms_SMSEevent2); } void sms_SMSEevent(object sender, EventArgs e) { Console.WriteLine("已經接到事件通知"); } void sms_SMSEevent2(object sender, SMSEventArgs e) { Console.WriteLine(e.ToPhoneNo + ":" + e.Message); } } //當然可以向一個事件注冊多個回調,如下: sms.SMSEevent2 += new EventHandler<SMSEventArgs>(sms_SMSEevent2_Other); void sms_SMSEevent2_Other(object sender, SMSEventArgs e) { Console.WriteLine("Other:" + e.ToPhoneNo + ":" + e.Message); }
到目前為止,關於事件及登記對象的定義已經完成了,下面我們來看一下編譯器是如何處理事件的。先看事件public event EventHandler SMSEevent;的定義:
關於這個事件SMSEevent的定義,C#編譯器自動生成了一個對應的字段private EventHandler SMSEevent;這個字段是具有對應委托類型的字段,它引用的是一個委托列表的頭部,當事件引用時,會通知這個委托列表中的成員。觀察該類構造可知,該委托被初始化為null,也就是當沒有事件登錄成員時,該委托為null,這也就說明了我們為什么在OnSMSEevent方法內要進行判空操作了。
C#編譯器同時生成了相關的兩個方法add_SMSEevent和remove_SMSEevent。在前面的章節《C#基礎知識梳理系列三:C#類成員:常量、字段、屬性》中講解過關於屬性的定義,C#編譯器是自動生成了get和set訪問器方法,這里跟它相似。
我們來看一下add_SMSEevent方法的定義:
public void add_SMSEevent(EventHandler value) { EventHandler handler2; EventHandler sMSEevent = this.SMSEevent; do { handler2 = sMSEevent; EventHandler handler3 = (EventHandler) Delegate.Combine(handler2, value); sMSEevent = Interlocked.CompareExchange<EventHandler>(ref this.SMSEevent, handler3, handler2); } while (sMSEevent != handler2); }
首先可以看到的是它是一個以循環的方式向事件追加委托,其次,它調用了Interlocked的靜態方法CompareExchange以線程安全的方式進行操作(Interlocked類被定義在System.Threading命名空間,其目的是為多個線程共享的變量提供原子操作。),方法內部還是調用Delegate.Combine方法來進行追加委托,因為這個事件在內部已經被當作委托(EventHandler)來看待的,從其字段定義可以看出。
有追加就有對應的移除,來看一下移除一個委托的方法remove_SMSEevent:
public void remove_SMSEevent(EventHandler value) { EventHandler handler2; EventHandler sMSEevent = this.SMSEevent; do { handler2 = sMSEevent; EventHandler handler3 = (EventHandler) Delegate.Remove(handler2, value); sMSEevent = Interlocked.CompareExchange<EventHandler>(ref this.SMSEevent, handler3, handler2); } while (sMSEevent != handler2); }
移除委托和上面的追加委托代碼結構相似,只是它是調用Delegate.Remove方法從委托鏈中移除一個委托,也是線程安全的。
編譯器有一個潛規則,那就是對事件的這兩個訪問器方法的命名都是有固定前綴,分別為add_和remove_,跟屬性的get_和set_方法相似。
還有一點,在內部是把事件當作委托看待,那么委托的追加和移除簡寫方式+=/-=在對事件也是同樣適用的,並且要求使用+=/-=的方式來注冊和移除監聽,當然,你也可以通過反射來動態為事件添加事件處理程序,而不使用+=/-=操作符。VS提供了對+=的快捷鍵處理,使用時,在對象的事件名后打上+=,VS的智能提示會提示“按Tab鍵插入”,按兩次Tab鍵后VS會自動生成事件處理程序,方便!體貼!
最后,我們來看一下如何在代碼中引用事件(部分代碼在上面已經定義):
void TestEvent() { SMS sms = new SMS(); Code_05_04 worker = new Code_05_04(sms); sms.SendSMS("1383838438", "自然災害已經離開地球,請放心生活。"); }
回顧上面的代碼,SMS類的方法SendSMS在內部會調用OnSMSEevent、OnSMSEevent2方法來引發事件,接着所有該事件的登記對象都會收到通知。如圖:
最后要說明一點的是,事件的通知是同步進行的,感興趣的同志可以自己寫代碼觀察一下。