事件也是方法。
定義一個事件成員意味著類型具有三種能力:
*類型的靜態方法/實例方法可以訂閱類型事件
*類型的靜態方法/實例方法可以注銷類型事件
*事件發生時通知已訂閱事件的方法
.NET2.0的事件仍然是基於Win32的,只不過使用了Observer模式來實現,同時建立在Delegate機制之 上。
事件的設計步驟如下(基本上是Observer的實現步驟):
10.1 設計一個對外提供事件的類型
1.定義EventArgs或子類,用於存放附加信息:
定義一個類,繼承於EventArgs,以EventArgs結束,包含一組私有字段以及相應的只讀公共屬性。
public class NewMailEventArgs : EventArgs { private string from; public string From { get { return from; } } }
這裡,EventArgs基類在FCL中是這個樣子的:
[Serializable] [ComVisible(true)] public class EventArgs { // Summary: // 表示沒有事件數據的事件。 public static readonly EventArgs Empty; public EventArgs(); }
大多數事件沒有附加數據,那麼就不用定義任何私有字段和屬性,直接使用EventArgs基類作為參數。
2.定義事件成員:
class MailManager { public event EventHandler<NewMailEventArgs> NewMail; }
這條語句等價於:
public delegate void EventHandler<TVEventArgs>(Object sender, TVEventArgs e) where TVEventArgs: NewMailEventArgs;
所以方法原型相應為 void MethodName(Object sender, NewMailEventArgs e)
這裡,第一個參數sender類型是Object,因為要兼容所有類型,所以提供一個最廣泛的基類型。
第二個參數名始終是e,而且派生於EventArgs,保持了對Observer模式的一致性,所有人(包括 VS2005)都會調用這個e
事件方法要求都為void,即不允許有回調值,從而事件鏈易於操作。
3.定義引發事件的方法——負責通知訂閱事件的對象:
這是一個protected的虛方法,並接受EventArgs或其子類的參數。
這個虛方法可以由派生類重寫,以添加新的功能;不重寫也可以,因為基本上已經可以使用了
class MailManager { protected virtual void OnNewMail(NewMailEventArgs e) { EventHandler<NewMailEventArgs> temp = NewMail; if (temp != null) temp(this, e); } }
這裡,使用臨時變量temp,是為了防止可能存在的線程同步問題。
4.定義一個激發事件的方法
將輸入轉換成EventArgs或其子類的對象,然後激發事件
internal class MailManager { public void SimulateNewMail(String from, String to, String subject) { NewMailEventArgs e = new NewMailEventArgs(from, to, subject); OnNewMail(e); } }
10.3 設計訂閱者的類,使用事件
在ctor中訂閱事件,綁定FaxMsg回調方法,在Unregister方法中注銷事件
提供回調方法FaxMsg,當事件激發時自動調用
internal sealed class Fax { public Fax(MailManager mm) { mm.NewMail += FaxMsg; } private void FaxMsg(Object sender, NewMailEventArgs e) { Console.WriteLine("Fax: {0}, {1}, {2}", e.From, e.To, e.Subject); } public void Unregister(MailManager mm) { mm.NewMail -= FaxMsg; } }
注意:使用+=和-=操作符,而不能顯示使用add/remove方法
事件注銷的意義:只要有一個對象還有一個方法仍然訂閱事件,該對象就不會被垃圾收集
IDispose接口的Dispose方法,注銷所有事件。
FaxMsg方法的sender參數為MailMessager對象,可以使用sender訪問MailMessager的對象成員,
補充:在Main函數中實現:
public static void Main() { MailManager mm = new MailManager(); //注冊pager和fax Fax fax = new Fax(mm); Pager pager = new Pager(mm); //通知pager和fax mm.SimulateNewMail("Jeffrey", "Kristin", "I Love You!"); //注銷fax,只剩下pager fax.Unregister(mm); //只通知pager mm.SimulateNewMail("Jeffrey", "Mom & Dad", "Happy Birthday."); }
10.2 事件機制
對於public event EventHandler<NewMailEventArgs> NewMail;
C#編譯時,相應為
//一個初始化為null的私有委托字段: private EventHandler<NewMailEventArgs> NewMail = null; //一個訂閱事件的公共方法: [MethodImpl(MethodImplOptions.Synchronized)] public void add_NewMail(EventHandler<NewMailEventArgs> value) { NewMail = (EventHandler<NewMailEventArgs>) Delegate.Combine(NewMail, value); } //一個注銷事件的公共方法: [MethodImpl(MethodImplOptions.Synchronized)] public void remove_NewMail(EventHandler<NewMailEventArgs> value) { NewMail = (EventHandler<NewMailEventArgs>) Delegate.Remove(NewMail, value); }
注:在IL中也是3個成員:一個私有字段,兩個公有方法
如果將event聲明為protected,則兩個方法也相應為protected
event也可以是static或virtual,則兩個方法也相應為static或virtual
10.4 事件與線程安全
在上面的實例中,System.Runtime.CompilerServices命名空間下,自定義屬性[MethodImpl (MethodImplOptions.Synchronized)]保證了事件的線程同步。
但是這樣的同步會有問題。
對於實例事件,CLR使用自身對象作為線程同步鎖;
對於靜態事件,CLR使用類型對象作為線程同步鎖。
但是線程同步指導方針指出,方法永遠不要在對象本身或類型對象上加鎖,否則這個鎖對外公開,會 導致其它線程死鎖
沒有好的辦法保證值類型的實例事件成員是線程安全的,因為C#不會為其add/remove生成[MethodImpl
(MethodImplOptions.Synchronized)];
值類型的靜態事件成員肯定是線程安全的。
10.5 顯示控制事件的訂閱與注銷
即顯示的實現add和remove訪問器方法:
建立一個臨時委托變量m_NewMail與相應的屬性,代替原先的事件成員NewMail,
新建一個作為線程同步鎖的私有實例字段m_eventLock
主要改動如下:
class MailManager { private EventHandler<NewMailEventArgs> m_NewMail; public event EventHandler<NewMailEventArgs> NewMail { add { lock (m_eventLock) { m_NewMail += value; } } remove { lock (m_eventLock) { m_NewMail -= value; } } } }
注意,C#不能分辨add/remove方法是由編譯器自動創建的,還是程序員顯示實現的,所以仍可以使用 +=和-=這兩個操作符處理事件。
10.6 多事件模型
System.Windows.Forms.Control類型有70多個事件,不可能用上述方法實現,會造成未使用事件對內 存的浪費。
解決辦法:使用注冊工廠,建立事件池。具體見設計模式。