事件也是方法。
定義一個事件成員意味著類型具有三種能力:
*類型的靜態方法/實例方法可以訂閱類型事件
*類型的靜態方法/實例方法可以注銷類型事件
*事件發生時通知已訂閱事件的方法
.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多個事件,不可能用上述方法實現,會造成未使用事件對內 存的浪費。
解決辦法:使用注冊工廠,建立事件池。具體見設計模式。