檔案涉及的內容:
設計公開事件類型
編譯器如何?事件
設計偵聽事件的類型
顯式實現事件
事件:定義了事件成員的類型允許類型通知其他對象發生特定的事情。
CLR事件模型以委託為基礎,委託是調用回調方法的一種型別安全的方式,對象憑藉調用方法接收他們訂閱的通知。
定義了事件成員的類型要求能夠提供以下功能:
方法能登記它對事件的關注
方法能登出它對事件的關注
事件發生時,登記的方法將收到通知
本文章以一個電子郵件應用程式為例。當電子郵件到達時,使用者希望將郵件轉寄給傳真機或呼叫器進行處理。先設計MainlManager類型來接收傳入的電子郵件,它公開NewMain事件。其他類型(Fax或Pager)對象登記對於該事件的關注。MailManager收到新電子郵件會引發該事件,造成郵件分發給每個已登記的對象,它們都有自己的方式處理郵件。
1.1設計要公開事件的類型
第一步:定義類型來容納所有需要發送給事件通知接收者的附加資訊
該類型通常包含一組私人欄位以及一些用於公開這些欄位的唯讀公用屬性。
1 class NewMailEventArgs:EventArgs 2 { 3 private readonly string m_from, m_to, m_subject; 4 public NewMailEventArgs(string from,string to,string subject) 5 { 6 m_from = from; 7 m_to = to; 8 m_subject = subject; 9 }10 public string From { get { return m_from; } }11 public string To { get{ return m_to; } }12 public string Subject { get { return m_subject; } }13 }
第二步:定義事件成員
class MailManager { public event EventHandler<NewMailEventArgs> NewMail; }
其中NewMail是事件名稱。事件成員類型是EventHandler<NewMailEventArgs>說明事件通知的所有接收者都必須提供一個原型和其委託類型匹配的回調方法。由於泛型System.EventHandler委託類型的定義如下:
public delegate void EventHandler<TEventArgs>(Object sender,TEventArgs e);
所以方法原型必須具有以下形式:void MethodName(Object sender,NewMailEventArgs e);之所以事件模式要求所有事件處理常式的傳回型別都是void,是因為引發事件後可能要調用好幾個回調方法,但沒辦法獲得所有方法的傳回值,返回void就不允許回調方法有傳回值。
第三步:定義負責引發事件的方法來通知事件的登錄物件
1 /// <summary> 2 /// 定義負責引發事件的方法來通知事件的登錄物件,該方法定義在MailManager中 3 /// 如果類是密封的,該方法要聲明為私人和非虛 4 /// </summary> 5 /// <param name="e"></param> 6 protected virtual void OnNewMail(NewMailEventArgs e) 7 { 8 //出於安全執行緒考慮,現在將委託欄位的引用複製到一個臨時變數中 9 EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);10 if(temp!=null)11 {12 temp(this, e);13 }14 }
上面方法使用了Volatile.Read()方法確保安全執行緒,主要考慮下面兩種情況:
1.直接判斷NewMail!=null,但在調用NewMail之前,另一個線程可能從委託鏈中移除了一個委託,使其為空白,從而發生(NullReferenceException)異常。
2.有些人可能也會將其儲存在一個臨時變數中,但未使用Volatile,理論上可以但是如果編譯器發生最佳化代碼移除該臨時變數,那就和第一種情況一樣。
使用Volatile.Read會強迫NewMail在這個調用發生時讀取,引用必須複製到temp變數中,比較完美的解決方式。但是在單線程的中不會出現這種情況
第四步 定義方法將輸入轉化為期望事件
1 public void SimulateNewMail(string from,string to,string subject)2 {3 //構造一個對象來容納想傳給通知接收者的資訊4 NewMailEventArgs e = new NewMailEventArgs(from, to, subject);5 //調用虛方法通知對象事件已反生6 //如果沒有類型重寫該方法7 //我們的對象將通知事件的所有登錄物件8 OnNewMail(e);9 }
該方法指出一封新的郵件已到達MailManager。
1.2 編譯器如何?事件
在MailManager類中我們用一句話定義事件成員本身:public event EventHandler<NewMailEventArgs> NewMail;
C#編譯器會轉換為以下代碼:
//一個被初始化為null的私人欄位 private EventHandler<NewMailEventArgs> NewMail = null; public void add_NewMail(EventHandler<NewMailEventArgs> value) { //通過迴圈和對CompareExchange的調用,以一種安全執行緒的方式向事件添加委託 //CompareExchange是把目標運算元(第1參數所指向的記憶體中的數) //與一個值(第3參數)比較,如果相等, //則用另一個值(第2參數)與目標運算元(第1參數所指向的記憶體中的數)交換 EventHandler<NewMailEventArgs> prevHandler; EventHandler<NewMailEventArgs> newMail = this.NewMail; do { prevHandler = newMail; EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>)Delegate.Combine(prevHandler, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler); } while (newMail != prevHandler); } public void remove_NewMail(EventHandler<NewMailEventArgs> value) { EventHandler<NewMailEventArgs> prevHandler; EventHandler<NewMailEventArgs> newMail = this.NewMail; do { prevHandler = newMail; EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>)Delegate.Remove(prevHandler, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler); } while (newMail != prevHandler); }
本執行個體中,add和remove方法可訪問性都是public是因為事件NewMail聲明為public,事件的可訪問性決定了什麼代碼能登記和登出對事件的關注。但無論如何只有類型本身才能訪問上述委託欄位NewMail。除了上述產生的程式碼,編譯器還會在託管程式集的中繼資料中建置事件定義記錄項。包含一些標誌和基礎委託類型。CLR本身並不使用這些中繼資料資訊運行時只需要訪問器方法。
1.3 設計偵聽事件的類型
如何定義一個類型來使用另一個類型提供的事件。以Fax類型為例:
internal class Fax { public Fax(MailManager mm) { //向MailManager的NewMail事件登記我們的回調方法 mm.NewMail += FaxMsg; } //新郵件到達,MailManager將調用這個方法 //sender表示MailManager對象,便於將資訊回傳給它 //e表示MailManager對象想傳給我們的附加事件資訊 private void FaxMsg(object sender, NewMailEventArgs e) { Console.WriteLine("Fax 的訊息from:{0} to:{1} subject:{2}", e.From, e.To, e.Subject); } /// <summary> /// 登出 /// </summary> /// <param name="mm"></param> public void Unregister(MailManager mm) { mm.NewMail -= FaxMsg; } }
電子郵件應用程式初始化時首先構造MailManager對象,並將對該對象的引用儲存到變數中。然後構造Fax對象,並將MailManager對象引用作為實參傳遞。在Fax構造器中,使用+=登記對NewMail事件的關注。
1.4 顯式實現事件
對於System.Windows.Forms.Control類型定義了大約70個事件。每個從Control衍生類別型建立對象都要浪費大量記憶體,而大多數我們只關心少數幾個事件。如何通過顯式實現事件來高效的實現提供了大量事件的類思路如下:
定義事件時:公開事件的每個對象都要維護一個集合(如字典)。集合將某種事件標識符作為健,將委託列表作為值。新物件建構時集合也是空白。登記對一個事件的關注會在集合中尋找事件的標識符。如果事件標識符存在,新委託就和這個事件的委託列表合并,否則就添加事件標識符和委託。
引發事件時:對象引發事件會在集合中尋找事件的標識符,如果沒有說明沒有對象登記對這個事件的關注,所以也沒委託需要回調。否則就調用與它關聯的委託列表。
1 public sealed class EventKey { } 2 public sealed class EventSet 3 { 4 //定義私人字典 5 private readonly Dictionary<EventKey, Delegate> m_events = 6 new Dictionary<EventKey, Delegate>(); 7 /// <summary> 8 /// 不存在添加,存在則和現有EventKey合并 9 /// </summary> 10 public void Add(EventKey eventKey,Delegate handler)11 {12 //確保操作唯一13 Monitor.Enter(m_events);14 Delegate d;15 //根據健擷取值16 m_events.TryGetValue(eventKey, out d);17 //添加或合并18 m_events[eventKey] = Delegate.Combine(d, handler);19 Monitor.Exit(m_events);20 }21 /// <summary>22 /// 刪除委託,在刪除最後一個委託時還需刪除字典中EventKey->Delegate23 /// </summary> 24 public void Remove(EventKey eventKey,Delegate handler)25 {26 Monitor.Enter(m_events);27 Delegate d;28 //TryGetValue確保在嘗試從集合中刪除不存在的EventKey時不會拋出異常29 if (m_events.TryGetValue(eventKey,out d))30 {31 d = Delegate.Remove(d, handler);32 if(d!=null)33 {34 //如果還有委託,就設定新的頭部35 m_events[eventKey] = d;36 }37 else38 {39 m_events.Remove(eventKey);40 }41 }42 Monitor.Exit(m_events);43 }44 /// <summary>45 /// 為指定的EventKey引發事件46 /// </summary> 47 public void Raise(EventKey eventKey,Object sender,EventArgs e)48 {49 Delegate d;50 Monitor.Enter(m_events);51 m_events.TryGetValue(eventKey, out d);52 Monitor.Exit(m_events);53 if(d!=null)54 {55 //利用DynamicInvoke,會向調用的回調方法查證參數的型別安全,56 //並調用方法,如果存在類型不符,就拋異常57 d.DynamicInvoke(new Object[] { sender, e });58 }59 }60 }
接下來定義類來使用EventSet
1 public class FooEventArgs : EventArgs { } 2 public class TypeWithLotsOfEvents 3 { 4 //用於管理一組"事件/委託" 5 private readonly EventSet m_eventSet = new EventSet(); 6 //受保護的屬性使衍生類別型能訪問集合 7 protected EventSet EventSet { get { return m_eventSet; } } 8 //構造一個靜態唯讀對象來標識這個事件 9 //每個對象都有自己的雜湊碼,以便在對象的集合中尋找這個事件的委託鏈表10 protected static readonly EventKey s_fooEventKey = new EventKey();11 //定義事件訪問器方法,用於在集合中增刪委託12 public event EventHandler<FooEventArgs> Foo13 {14 add { m_eventSet.Add(s_fooEventKey, value); }15 remove { m_eventSet.Remove(s_fooEventKey, value); }16 }17 //為這個事件定義受保護的虛方法18 protected virtual void OnFoo(FooEventArgs e)19 {20 m_eventSet.Raise(s_fooEventKey, this, e);21 }22 //定義將輸入轉換成這個事件的方法23 public void SimulateFoo() { OnFoo(new FooEventArgs()); }24 }
如何使用TypeWithLotsOfEvent,只需按照標準的文法向事件登記即可
1 static void Main(string[] args) 2 { 3 TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents(); 4 twle.Foo += HandleFooEvent; 5 twle.SimulateFoo(); 6 Console.Read(); 7 } 8 9 private static void HandleFooEvent(object sender, FooEventArgs e)10 {11 Console.WriteLine("成功");12 }