[日期:2005-01-22] | 來源:部落格堂 作者:破寶 | [字型:大 中 小] |
在第一節中,我們討論了 .NET 事件模型的基本實現方式。這一部分我們將學習 C# 語言提供的高階實現方式:使用 add/remove 訪問器宣告事件。(注:本節內容不適用於 VB.NET。)
我們再來看看上一節中我們宣告事件的格式:
public event [委託型別] [事件名稱];
這種宣告方法,類似於類中的欄位(field)。無論是否有事件處理程式掛接,它都會佔用一定的記憶體空間。一般情況中,這樣的記憶體消耗或許是微不足 道的;然而,還是有些時候,記憶體開銷會變得不可接受。比如,類似 System.Windows.Forms.Control 型別具有五六十個事件,這些事件並非每次都會掛接事件處理程式,如果每次都無端的多處這麼多的記憶體開銷,可能就無法容忍了。
好在 C# 語言提供了“屬性”樣式的事件宣告方式:
public event [委託型別] [事件名稱] { add { .... } remove { .... } }
如上的格式宣告事件,具有 add 和 remove 訪問器,看起來就像屬性宣告中的 get 和 set 訪問器。使用特定的儲存方式(比如使用 Hashtable 等集合結構),通過 add 和 remove 訪問器,自定義你自己的事件處理程式新增和移除的實現方法。
Demo 1G:“屬性”樣式的事件宣告。我首先給出一種實現方案如下(此實現參考了 .NET Framework SDK 文件中的一些提示)(限於篇幅,我只將主要的部分貼在這裡):
public delegate void StartWorkEventHandler(object sender, StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender, RateReportEventArgs e);
// 注意:本例中的實現,僅支援“單播事件”。
// 如需要“多播事件”支援,請參考 Demo 1H 的實現。
// 為每種事件生成一個唯一的 object 作為鍵
static readonly object StartWorkEventKey = new object();
static readonly object EndWorkEventKey = new object();
static readonly object RateReportEventKey = new object();
// 使用 Hashtable 儲存事件處理程式
private Hashtable handlers = new Hashtable();
// 使用 protected 方法而沒有直接將 handlers.Add / handlers.Remove
// 寫入事件 add / remove 訪問器,是因為:
// 如果 Worker 具有子類的話,
// 我們不希望子類可以直接訪問、修改 handlers 這個 Hashtable。
// 並且,子類如果有其他的事件定義,
// 也可以使用基類的這幾個方法方便的增減事件處理程式。
protected void AddEventHandler(object eventKey, Delegate handler)
{
lock(this)
{
if (handlers[ eventKey ] == null)
{
handlers.Add( eventKey, handler );
}
else
{
handlers[ eventKey ] = handler;
}
}
}
protected void RemoveEventHandler(object eventKey)
{
lock(this)
{
handlers.Remove( eventKey );
}
}
protected Delegate GetEventHandler(object eventKey)
{
return (Delegate) handlers[ eventKey ];
}
// 使用了 add 和 remove 訪問器的事件宣告
public event StartWorkEventHandler StartWork
{
add { AddEventHandler(StartWorkEventKey, value); }
remove { RemoveEventHandler(StartWorkEventKey); }
}
public event EventHandler EndWork
{
add { AddEventHandler(EndWorkEventKey, value); }
remove { RemoveEventHandler(EndWorkEventKey); }
}
public event RateReportEventHandler RateReport
{
add { AddEventHandler(RateReportEventKey, value); }
remove { RemoveEventHandler(RateReportEventKey); }
}
// 此處需要做些相應調整
protected virtual void OnStartWork( StartWorkEventArgs e )
{
StartWorkEventHandler handler =
(StartWorkEventHandler) GetEventHandler( StartWorkEventKey );
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
EventHandler handler =
(EventHandler) GetEventHandler( EndWorkEventKey );
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
RateReportEventHandler handler =
(RateReportEventHandler) GetEventHandler( RateReportEventKey );
if (handler != null)
{
handler(this, e);
}
}
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
OnStartWork(new StartWorkEventArgs(MAX) );
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
OnRateReport( new RateReportEventArgs(rate) );
}
OnEndWork( EventArgs.Empty );
}
細細研讀這段程式碼,不難理解它的演算法。這裡,使用了名為 handlers 的 Hashtable 儲存外部掛接上的事件處理程式。每當事件處理程式被“add”,就把它加入到 handlers 裡儲存;相反 remove 時,就將它從 handlers 裡移除。這裡取 event 的 key (開始部分為每一種 event 都生成了一個 object 作為代表這種 event 的 key)作為 Hashtable 的鍵。
[TOP]
在 Demo 1G 給出的解決方案中,你或許已經注意到:如果某一事件被掛接多次,則後掛接的事件處理程式,將改寫先掛接的事件處理程式。這裡就涉及到一個概念,叫“單播事件”。
所謂單播事件,就是物件(類)發出的事件通知,只能被外界的某一個事件處理程式處理,而不能被多個事件處理程式處理。也就是說,此事件只能被掛接一次,它只能“傳播”到一個地方。相對的,就有“多播事件”,物件(類)發出的事件通知,可以同時被外界不同的事件處理程式處理。
打個比方,上一節開頭時張三大叫一聲之後,既招來了救護車,也招來了警察叔叔(問他是不是回不了家了),或許還有電視轉播車(現場直播、採訪張三為什麼大叫,呵呵)。
多播事件會有很多特殊的用法。如果以後有機會向大家介紹 Observer 模式,可以看看 Observer 模式中是怎麼運用多播事件的。(注:經我初步測試,欄位形式的事件宣告,預設是支援“多播事件”的。所以如果在事件種類不多時,我建議你採用上一節中所講 的欄位形式的宣告方式。)
[TOP]
Demo1H,支援多播事件。為了支援多播事件,我們需要改進儲存結構,請參考下面的演算法:
public delegate void StartWorkEventHandler(object sender, StartWorkEventArgs e); public delegate void RateReportEventHandler(object sender, RateReportEventArgs e); // 為每種事件生成一個唯一的鍵 static readonly object StartWorkEventKey = new object(); static readonly object EndWorkEventKey = new object(); static readonly object RateReportEventKey = new object(); // 為外部掛接的每一個事件處理程式,生成一個唯一的鍵 private object EventHandlerKey { get { return new object(); } } // 對比 Demo 1G, // 為了支援“多播”, // 這裡使用兩個 Hashtable:一個記錄 handlers, // 另一個記錄這些 handler 分別對應的 event 型別(event 的型別用各自不同的 eventKey 來表示)。 // 兩個 Hashtable 都使用 handlerKey 作為鍵。 // 使用 Hashtable 儲存事件處理程式 private Hashtable handlers = new Hashtable(); // 另一個 Hashtable 儲存這些 handler 對應的事件型別 private Hashtable events = new Hashtable(); protected void AddEventHandler(object eventKey, Delegate handler) { // 注意新增時,首先取了一個 object 作為 handler 的 key, // 並分別作為兩個 Hashtable 的鍵。 lock(this) { object handlerKey = EventHandlerKey; handlers.Add( handlerKey, handler ); events.Add( handlerKey, eventKey); } } protected void RemoveEventHandler(object eventKey, Delegate handler) { // 移除時,遍歷 events,對每一個符合 eventKey 的項, // 分別檢查其在 handlers 中的對應項, // 如果兩者都吻合,同時移除 events 和 handlers 中的對應項。 // // 或許還有更簡單的演算法,不過我一時想不出來了 :( lock(this) { foreach ( object handlerKey in events.Keys) { if (events[ handlerKey ] == eventKey) { if ( (Delegate)handlers[ handlerKey ] == handler ) { handlers.Remove( handlers[ handlerKey ] ); events.Remove( events[ handlerKey ] ); break; } } } } } protected ArrayList GetEventHandlers(object eventKey) { ArrayList t = new ArrayList(); lock(this) { foreach ( object handlerKey in events.Keys ) { if ( events[ handlerKey ] == eventKey) { t.Add( handlers[ handlerKey ] ); } } } return t; } // 使用了 add 和 remove 訪問器的事件宣告 public event StartWorkEventHandler StartWork { add { AddEventHandler(StartWorkEventKey, value); } remove { RemoveEventHandler(StartWorkEventKey, value); } } public event EventHandler EndWork { add { AddEventHandler(EndWorkEventKey, value); } remove { RemoveEventHandler(EndWorkEventKey, value); } } public event RateReportEventHandler RateReport { add { AddEventHandler(RateReportEventKey, value); } remove { RemoveEventHandler(RateReportEventKey, value); } } // 此處需要做些相應調整 protected virtual void OnStartWork( StartWorkEventArgs e ) { ArrayList handlers = GetEventHandlers( StartWorkEventKey ); foreach(StartWorkEventHandler handler in handlers) { handler(this, e); } } protected virtual void OnEndWork( EventArgs e ) { ArrayList handlers = GetEventHandlers( EndWorkEventKey ); foreach(EventHandler handler in handlers) { handler(this, e); } } protected virtual void OnRateReport( RateReportEventArgs e ) { ArrayList handlers = GetEventHandlers( RateReportEventKey ); foreach(RateReportEventHandler handler in handlers) { handler(this, e); } }
上面給出的演算法,只是給你做參考,應該還有比這個實現更簡單、更高效的方式。
為了實現“多播事件”,這次使用了兩個 Hashtable:一個儲存“handlerKey - handler”對,一個儲存“handlerKey - eventKey”對。相信通過仔細研讀,你可以讀懂這段程式碼。我就不再贅述了。