定義一個事件成員,表示該類型提供了如下功能:
1.能夠在事件中注冊方法 2.能夠在事件中注銷方法 3.當事件發(fā)生時,注冊的方法會被通知
(事件內(nèi)部維護了一個注冊方法列表)
CLR的事件模型是基于委托的,它可以通過類型安全的方式調(diào)用回調(diào)方法。而回調(diào)方法是訂閱事件的對象接收通知的方式。通過一個例子來說明:
①Fax對象的方法注冊到MailManager事件 ②Pager對象的方法注冊到MailManager事件 ③新的郵件到達MailManager ④MailManager對象向注冊的方法發(fā)出通知,接收通知的方法可以隨意處理。
具體實現(xiàn)步驟如下:
1.定義一個類型,能夠hold住任何發(fā)送到事件通知接收者的信息
當一個事件被觸發(fā),觸發(fā)事件的對象可能希望發(fā)送一些額外的信息給事件通知的接收對象。這些額外的信息需要封裝在它自己的類中,根據(jù)約定該類需要從System.EventArgs類派生,并且命名以EventArgs結尾。這里定義一個NewMailEventArgs類:
public class NewMailEventArgs : EventArgs { private readonly String m_from, m_to, m_subject; public NewMailEventArgs(String from, String to, String subject) { m_from = from; m_to = to; m_subject = subject; } public String From { get { return m_from; } } public String To { get { return m_to; } } public String Subject { get { return m_subject; } } }
關于EventArgs
[ComVisible(true)] [Serializable] public class EventArgs { public readonly static EventArgs Empty; static EventArgs() { EventArgs.Empty = new EventArgs(); } public EventArgs() { } }
這個類沒有實際的用途,只是作為一個基類讓其他對象繼承。很多對象不需要傳遞額外的信息,例如按鈕事件,只是調(diào)用一個回調(diào)方法就夠了。當我們定義的事件不需要傳遞額外的信息時,這時調(diào)用EventArgs.Empty就行了,不需要重新構建一個EventArgs對象。
2.定義事件成員
public class MailManager
{
...
//NewMail事件名,
//EventHanlder<NewMailEventArgs>,所有的事件通知接收對象必須提供給該委托類型匹配的回調(diào)方法
public event EventHandler<NewMailEventArgs> NewMail;
}
System.EventHandler委托的定義為:public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e) where TEventArgs: EventArgs;
為什么這里第一個參數(shù)sender的類型是Object?畢竟MailManager類型是唯一觸發(fā)這個事件的,所以可以設計成這樣:
void MethodName(MailManager sender,NewMailEventArgs e)
這種情況會有一個弊端,當sender是SmtpMailManager時,回調(diào)方法也需要改變,使用Object能夠很好的兼容。定義回調(diào)方法的參數(shù)名約定為e,這樣做主要是為了保持一致性。方便開發(fā)人員。
事件機制要求所有的事件處理方法必須返回void,這是必要的,因為一個事件可能觸發(fā)很多的回調(diào)方法,沒有辦法獲取所有的返回值,索性就不允許返回值,全部為void。有些FCL里面的事件處理程序沒有遵循,而是返回了一個Assembly類型。
3.定義一個方法來響應事件的發(fā)生
按照慣例,這個類應該定義一個protected,virtual的方法供內(nèi)部的代碼調(diào)用。這個方法接收一個NewMailEventArgs對象,這個對象包含要傳遞給消息接收方的一些信息。如下:
protected virtual void OnNewMail(NewMailEventArgs e)
{
//復制一個委托的引用到臨時字段temp,這樣確保線程安全
EventHandler<NewMailEventArgs> temp = Interlocked.CompareExchange(ref NewMail, null, null);
//任何注冊到事件里面的方法,通知它們
if (temp != null)
{
temp(this, e);
}
}
Tips:使用線程安全的方式觸發(fā)事件(①——>④為不斷改進的過程)
①當.NET第一次推出的時候,給開發(fā)者推薦的事件觸發(fā)方式如下:
//v1.0
protected virtual void OnNewMail(NewMailEventArgs e)
{
if (NewMail != null)
{
NewMail(this, e);
}
}
弊端:這里檢查了NewMail不為null才觸發(fā),但是當檢查完之后,在調(diào)用NewMail之前,有其他的線程從委托鏈中移除了一個委托,使得NewMail為null,此時會拋出異常。
②先將NewMail用一個臨時變量存起來,這時就不會因為調(diào)用時被其他線程修改而拋出異常。之所以能夠這樣做,是因為委托類型跟字符串類型一樣是不可變的。
//v2.0
protected void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = NewMail;
if (temp != null)
{
temp(this, e);
}
}
弊端:可能被編譯器優(yōu)化掉本地temp變量,如果發(fā)生這種情況,就回到了第一種了。
③修復上面的bug,如下:
//v3.0
protected void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = Thread.VolatileRead(ref NewMail);
if (temp != null)
{
temp(this, e);
}
}
這里使用VolatileRead會強制讀取temp的值,但是這里不能這樣寫,編譯不通過。但是有一個Interlocked.CompareExchange可以使用:
④
//v4.0
protected virtual void OnNewMail(NewMailEventArgs e)
{
//復制一個委托的引用到臨時字段temp,這樣確保線程安全
EventHandler<NewMailEventArgs> temp = Interlocked.CompareExchange(ref NewMail, null, null);
//任何注冊到事件里面的方法,通知它們
if (temp != null)
{
temp(this, e);
}
}
如果NewMail為null,CompareExchange將NewMail的值改變?yōu)閚ull,如果不為null則返回原值。換句話說,CompareExchange不會改變NewMail的值,只是以線程安全的方式返回NewMail的值,這里是一個原子操作。
第④個版本是最佳的,技術上最正確的版本。實際開發(fā)中還是可以使用第②個版本,因為JIT編譯器能夠識別這種模式而不去優(yōu)化本地的temp變量。特別地,所有微軟的JIT編譯器都遵循不會對堆引入新的讀取,因此緩存一個引用在本地變量可以確保堆引用只被訪問一次(這是沒有寫入文檔的,理論上,還是可能發(fā)生變化,所以最好選用第④版本。)
為了方便可以定義一個擴展方法來封裝:
public static class EventArgExtensions
{
public static void Raise<TEventArgs>(this TEventArgs e, Object sender, ref EventHandler<TEventArgs> eventDelegate)
where TEventArgs : EventArgs
{
EventHandler<TEventArgs> temp = Interlocked.CompareExchange(ref eventDelegate, null, null);
if (temp != null)
{
temp(sender, e);
}
}
}
然后可以重寫OnNewMail:
protected virtual void OnNewMail(NewMailEventArgs e) { e.Raise(this, ref NewMail); }
4.定義一個方法用來傳遞一些輸入到事件
public void SimulateNewMail(String from, String to, String subject) { NewMailEventArgs e = new NewMailEventArgs(from, to, subject); OnNewMail(e); }
本文導航