(原創翻譯文章·轉載請注明來源:http://blog.csdn.net/hulihui)
- 原文:Weak Events In C#: Different approaches to weak events. by Daniel Grunwald.
Download source code - 15.5 KB
- 翻譯前序
- 翻譯后記
目錄
- 引言
- 究竟什么是事件?
- 第1部分:監聽方(Listener-side)的弱事件
- 解決方案0:僅僅注銷
- 解決方案1:事件調用后注銷
- 解決方案2:帶弱引用(WeakReference)的包裝器
- 解決方案3:終結器(Finalizer)中注銷
- 解決方案4:可重復使用的包裝器
- 解決方案5:弱事件管理器(WeakEventManager)
- 第2部分:事件源(Source-side)的弱事件
- 解決方案0:接口
- 解決方案1:弱引用委托
- 解決方案2:對象+轉發器(Forwarder)
- 解決方案3:智能弱事件(SmartWeakEvent)
- 解決方案4:快速智能弱事件(FastSmartWeakEvent)
- 建議
翻譯前序
本文涉及到的.NET 2.0的內容包括:委托(delegate)、事件(event)、強引用(strong reference)、弱引用(weak reference)、終結器(finalizer)、垃圾收集器(garbage collector)、閉環對象(closure object)、反射(reflect)、線程安全(thread safe)、內存泄露(leak),等等。進一步理解需要.NET 3.0/3.5/4.0的幾個概念:弱事件(weak event)、弱事件管理器(WeakEventManager)、lambda表達式、分派器(dispatcher),等等。
引言
使用正常C#事件情況時,注冊一個事件處理程序(handler)就是創建一個從事件源到到監聽對象的強引用。
如果事件源對象比監聽者對象具有更長的生存期,且事件監聽者沒有被其它對象引用也不再需要該事件,這時使用正常的.NET事件將導致內存泄漏:事件源對象在內存中保持了應該被垃圾(garbage)回收的監聽對象的引用。
這類問題存在許多不同的解決方法。本文將解釋其中的一些方法,探討它們的優缺點。我將這些方法分為兩類:首先,我們假設事件源是一個有正常C#事件的類;然后,我們允許修改事件源以適應不同的方法。
究竟什么是事件?
許多程序員認為事件是委托鏈表。這是完全錯誤的。事實上,委托自己有“多播”(multi-cast)能力:
EventHandler eh = Method1;
eh += Method2;
那么,什么是事件?初步看,它們類似屬性(properties):封裝一個委托字段並限制其訪問。通常情況下,一個公共委托字段(或公共委托屬性)意味着其它對象可以清除事件處理程序或激發事件,而我們只希望事件的定義者具有有這種操作能力。本質上,屬性是一對get/set方法、事件是一對add/remove方法。
public event EventHandler MyEvent
{
add {...}
remove {...}
}
上述代碼中,只有增加與移除操作是公開的,其它類不能請求執行處理程序鏈表,不能清除鏈表,也不能調用事件。使用這種形式帶來的問題是,C#事件簡寫語法有時引起編程者的困惑:
public event EventHandler MyEvent;
進一步擴展到下面情況:
private EventHandler _MyEvent; // 下划線起頭的字段
// 它不是實際的命名"_MyEvent",而是"MyEvent",
// 於是你也不能區分字段和事件。
public event EventHandler MyEvent
{
add { lock (this) { _MyEvent += value; } }
remove { lock (this) { _MyEvent -= value; } }
}
值得注意的是,默認的C#事件是對this加鎖的,可以使用一個反匯編器(disassembler)驗證這一點:add/remove方法標記了屬性[MethodImpl(MethodImplOptions.Synchronized)],這等價於對this加鎖。這樣,注冊和注銷事件是線程安全的。然而,以線程安全方式激發事件的編碼工作交由程序員實現,而他們往往做得不對——通常情況下可能使用的代碼不是線程安全的:
if (MyEvent != null)
MyEvent(this, EventArgs.Empty);
// 當最后的事件處理程序並發移除導致
// NullReferenceException時系統可能崩潰。
第二個常見的策略是先讀取事件委托到一個局部變量中:
EventHandler eh = MyEvent;
if (eh != null) eh(this, EventArgs.Empty);
這是線程安全的嗎?答案:還要看。根據C#規范中的內存模型,這也不是線程安全的。JIT編譯器允許消去這個局部變量(參見“理解多線程應用中的低鎖技術影響”(Understand the Impact of Low-Lock Techniques in Multithreaded Apps))。然而,從2.0版開始微軟.NET運行時有更強的內存模型,這時上述碼又是線程安全的。碰巧的是,在微軟.NET1.0和1.1上它也是線程安全的,但是其實現細節沒有在相關文檔中說明。
根據歐洲計算機制造商協會(ECMA)規范,一個正確的解決方法是把局部變量賦值語句移到lock(this)塊中,或者使用易失性(volatile)字段保存這個委托。
EventHandler eh; EventHandler;
lock (this) { eh = MyEvent; }
if (eh != null) eh(this, EventArgs.Empty);
於是,我們不得不區分:線程安全的事件、非線程安全的事件。
第1部分:監聽方(Listener-side)的弱事件
在這一部分中假設事件是一個正常的C#事件(強引用事件處理程序),且任何清理工作都在監聽方完成。
解決方案0:僅僅注銷
void RegisterEvent()
{
eventSource.Event += OnEvent;
}
void DeregisterEvent()
{
eventSource.Event -= OnEvent
}
void OnEvent(object sender, EventArgs e)
{
...
}
上面就是我們經常用到的簡單有效的形式。然而,當對象不再使用時,通常不能確保DeregisterEvent方法被調用。可以嘗試用Dispose模式(它通常意味着非托管資源),但終結器(Finalizer)不會被執行:垃圾收集器不會調用這個終結器,因為事件源仍然保持了監聽對象的引用!
優點:
如果對象已經標記為disposed就簡單(意味着可以調用Filalizer了——譯者注)。
缺點:
顯式內存管理較難,可能忘記調用Dispose。
解決方案1:事件調用后注銷
void RegisterEvent()
{
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
if (!InUse) {
eventSource.Event -= OnEvent;
return;
}
...
}
現在,不需要有人指出何時監聽者不再使用:事件調用時它只需要檢查自己即可。然而,如果我們不能使用解決方案0,那么通常情況下也無法從監聽對象中確定InUse。假如你正在閱讀本文,您可能已經遇到過其中的一個情形了。
但是,比較解決方案0,這個“解決方案”已經有一個嚴重的缺點了:如果事件是從未激發(即OnEvent從未被調用——譯者注),那么也將泄漏監聽對象。想象這種情況,許多對象注冊到一個靜態“SettingsChanged”事件上——所有這些對象將不能被垃圾回收,直到一個設置改變——在程序的生存期內這種設置改變或許永遠不會發生。
優點:
-
缺點:
當事件從未激發時內存泄漏,通常情況下“InUse”不易確定。
解決方案2:帶弱引用(WeakReference)的包裝器
這個解決方案幾乎等同於前一個,區別在於:我們把事件處理代碼移到一個包裝器類中,該包裝器類轉發調用到一個弱引用(有關弱引用的概念請參考(WeakReference)——譯者注)的監聽者實例。監聽者存活時,這個弱引用將容易被檢測到。
EventWrapper ew;
void RegisterEvent()
{
ew = new EventWrapper(eventSource, this);
}
void OnEvent(object sender, EventArgs e)
{
...
}
sealed class EventWrapper
{
SourceObject eventSource;
WeakReference wr;
public EventWrapper(SourceObject eventSource, ListenerObject obj)
{
this.eventSource = eventSource;
this.wr = new WeakReference(obj); // 創建一個ListenerObj的弱引用——譯者注
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
ListenerObject obj = (ListenerObject)wr.Target; // 獲取Listener對象——譯者注
if (obj != null)
obj.OnEvent(sender, e);
else
Deregister();
}
public void Deregister()
{
eventSource.Event -= OnEvent;
}
}
優點:
允許垃圾回收監聽對象。
缺點:
事件從未激發時泄漏包裝器實例,為每個事件處理程序寫一個包裝器類將重復大量代碼。
解決方案3:終結器(Finalizer)中注銷
請注意,上述方案中儲存了一個EventWrapper引用,並有一個公有方法Deregister,可以給監聽者增加一個終結器(Finalizer),它可以調用包裝器的注銷方法。
~ListenerObject() {
ew.Deregister();
}
這個方案顧全了內存泄漏問題,但是有代價的:對垃圾回收器而言,可終結對象是高代價的。當沒有監聽對象引用時(除弱引用外),它將在第一次垃圾回收時生存下來並升級一代。假設終結器運行,在接下來的第二次垃圾收集時被回收(新一代的對象)。此外,終結器運行在終結器線程上,如果注冊/注銷事件的事件源不是線程安全的,也可能引發問題。請記住,C#編譯器產生的默認事件不是線程安全的!
優點:
允許垃圾回收監聽對象、不會漏包裝器實例。
缺點:
終結器延時GC監聽者、需要線程安全的事件源、大量重復代碼。
解決方案4:可重復使用的包裝器
下載代碼中包含一個可重復使用的包裝器類(WeakEventHandler),並使用lambda表達式以適應特定的應用情況:注冊事件處理程序、注銷事件處理程序、轉發事件給私有方法。
eventWrapper = WeakEventHandler.Register(
eventSource,
(s, eh) => s.Event += eh, // 注冊代碼
(s, eh) => s.Event -= eh, // 注銷代碼
this, // 事件監聽者
(me, sender, args) => me.OnEvent(sender, args) // 轉發代碼
);
返回的eventWrapper暴露了單一公共方法:Deregister。現在,我們必須小心處理lambda表達式,因為它們編譯成可能引用其它對象的委托,這也是事件監聽者傳遞“me”的原因。假設我們寫成(me, sender, args) => this.OnEvent(sender, args), 這個lambda表達式將捕獲”this“變量,從而產生一個閉環對象(closure object)。因為WeakEventHandler存儲了一個轉發委托的引用,這將導致一個從包裝器到監聽者的強引用。幸運的是,它可以檢查是否一個委托捕獲到了任何變量:編譯器將為lambda表達式生成一個捕獲變量的實例方法,以及一個不捕獲變量的靜態方法。WeakEventHandler使用Delegate.Method.IsStatic檢查這種情況,並在使用不當時拋出異常。
這種做法是高度可重復使用的,但對每個委托類型它仍然需要一個包裝器類。當使用System.EventHandler和System.EventHandler<T>做得得心應手時,我們也許想自動完成這項工作,特別是有許多不同的委托類型時。這可以在編譯時使用代碼生成,或在運行時使用System.Reflection.Emit完成。
優點:
允許垃回收監聽對象;代碼開銷不算太差。
缺點:
事件從未激發時泄漏包裝器實例。
解決方案5:弱事件管理器(WeakEventManager)
WPF內置的WeakEventManager類支持監聽方弱事件,它類似前面的包裝器解決方案,區別在於:一個單一WeakEventManager實例充當了多個發送者和多個監聽者之間的包裝器。由於是單一實例,WeakEventManager可避免事件從未調用時的泄漏現象:在WeakEventManager上注冊另一個事件時可以觸發舊事件的清理工作。這些清理由WPF分派者(dispatcher)調度,且運行在WPF消息循環線程上。
此外,WeakEventManager有一個前面解決方案沒有的限制:要求正確設置發送者參數。使用它附加button.Click時,只有sender==button的事件才能被傳遞轉發。注意,WeakEventManager不適用於如下類型的事件:簡單附加處理程序到另一個事件:
public event EventHandler Event {
add { anotherObject.Event += value; }
remove { anotherObject.Event -= value; }
}
每個事件有一個WeakEventManager類,每個線程一個實例。定義這類事件時建議參考一個大的樣板模式代碼: 見MSDN上的“WeakEvent模式”(WeakEvent Patterns)。幸運的是,我們可以使用泛型來簡化這項工作:
public sealed class ButtonClickEventManager
: WeakEventManagerBase<buttonclickeventmanager, button="">
{
protected override void StartListening(Button source)
{
source.Click += DeliverEvent;
}
protected override void StopListening(Button source)
{
source.Click -= DeliverEvent;
}
}
請注意,DeliverEvent具有簽名(object, EventArgs),而Click事件提供(object, RoutedEventArgs)。雖然委托類型之間沒有轉換關系,然而C#從方法組中創建委托時支持逆變(contravariance when creating delegates from method groups) 。
優點:
允許垃圾回收監聽對象,不漏包裝器實例。
缺點:
綁定WPF分派者,非UI線程上不易使用。
第2部分:事件源(Source-side)的弱事件
這里將探討修改事件源實現弱事件的各種方法。對比監聽方的弱事件,所有這些方法都有一個共同的優點:可以較容易地進行線程安全的注冊/注銷事件處理程序。
解決方案0:接口
本節還得提及WeakEventManager:作為包裝器,它附加(“listening-side”)到正常C#事件,也提供(“source-side”)一個弱事件給客戶端。WeakEventManager中定義IWeakEventListener接口,監聽對象實現接口,事件源只需擁有一個監聽者弱引用並調用接口方法即可。
優點:
簡單有效。
缺點:
當監聽者處理多個事件時,HandleWeakEvent方法中附有許多過濾事件類型與事件源的條件。
解決方案1:弱引用委托
這是WPF中處理弱事件的另一種辦法:CommandManager.InvalidateRequery看起來像正常的.NET事件,但事實並非如此:它只保持委托的弱引用,注冊到這個靜態事件不會造成內存泄漏。
雖然這是一個簡單的解決方案,但事件消費者容易忘記使用也容易誤用:
CommandManager.InvalidateRequery += OnInvalidateRequery;
// 或
CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery);
問題是CommandManager只有委托的弱引用,且監聽者沒有引用它。因此,在GC的下一次運行時,委托將被垃圾回收,並且OnInvalidateRequery不能再被調用,即使監聽對象仍在使用。為了確保委托存活足夠長的時間,監聽者負責維持對它的引用。
class Listener {
EventHandler strongReferenceToDelegate;
public void RegisterForEvent()
{
strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
CommandManager.InvalidateRequery += strongReferenceToDelegate;
}
void OnInvalidateRequery(...) {...}
}
下載代碼中的WeakReferenceToDelegat給出了一個事件實現例子,它是線程安全的,當增加另一個處理程序時清除處理程序鏈表。
優點:
不泄露委托實例。
缺點:
容易誤用:忘記委托的強引用,僅當下次垃圾回收時激發事件,可能會造成bugs發現困難。
解決方案2:對象+轉發器(Forwarder)
WeakEventManager采用了解決方案0,而本解決方案采用了WeakEventHandler包裝器:注冊一個(object,ForwarderDelegate)對:
eventSource.AddHandler(this, eventSource.AddHandler
(me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));
優點:
簡單有效。
缺點:
非常規簽名方式注冊事件,轉發lambda表達式需要類型轉換(cast)。
解決方案3:智能弱事件(SmartWeakEvent)
下載代碼的SmartWeakEvent提供了一個類似正常.NET事件的事件,它保持了事件監聽者的弱引用,但不受“必須保持委托引用”問題的困擾。
void RegisterEvent()
{
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
...
}
事件定義:
SmartWeakEvent _event
= new SmartWeakEvent();
public event EventHandler Event
add { _event.Add(value); }
remove { _event.Remove(value); }
}
public void RaiseEvent()
{
_event.Raise(this, EventArgs.Empty);
}
如何工作?使用Delegate.Target和Delegate.Method屬性,把每個委托分成一個目標(存儲為一個弱應用)和MethodInfo ,事件激發時用反射調用該方法。
這里的一個可能問題是:有人可能會附加一個匿名方法作為事件處理程序,並在匿名方法中捕獲一個變量。
int localVariable = 42;
eventSource.Event += delegate { Console.WriteLine(localVariable); };
在這種情況下,委托目標對象是閉環的(closure)、可以立即垃圾回收,因為沒有其它對象引用它。然而,SmartWeakEvent能夠檢測這種情況下並拋出一個異常,所以不會有任何調試上的困難,因為事件處理程序在我們認為應該注銷之前已經注銷了。
if (d.Method.DeclaringType.GetCustomAttributes(
typeof (CompilerGeneratedAttribute), false ).Length != 0)
throw new ArgumentException(...);
優點:
似乎是一個真正的弱事件;幾乎沒有代碼開銷。
缺點:
反射調用速度慢。
解決方案4:快速智能弱事件(FastSmartWeakEvent)
功能和使用與SmartWeakEvent相同,但顯著改善了性能。下面是有兩個注冊委托(一個實例的方法和一個靜態方法)的事件的測試結果:
Normal (strong) event... 16948785 調用每秒
Smart weak event... 91960 調用每秒
Fast smart weak event... 4901840 調用每秒
如何工作?不再使用反射調用方法,而在運行時使用System.Reflection.Emit.DynamicMethod編譯一個轉發器方法(類似前面方案的“轉發代碼”)。
優點:
似乎是一個真正的弱事件;幾乎沒有代碼開銷。
缺點:
-
建議
- 運行在WPF的UI線程上的任何對象(例如,附加事件到定制控件),使用WeakEventManager;
- 如果想提供一個弱事件,使用FastSmartWeakEvent;
- 如果想消費一個事件,使用WeakEventHandler。
翻譯后記
最近,特別關注.NET上的委托和事件及相關實現技術。瀏覽codeproject時看到一篇關於Weak Events的文章,因好奇這個概念就多讀了幾遍,發現其中的一些構思和方法比較有深度和技巧,也澄清了幾個在事件概念上的誤解和模糊點。該文主要探討.NET 3.0及以后平台的實現技術。但是其中的基本思想(如:WeakReference)還是可以在.NET 2.0及以上平台上應用。文章內容深奧難懂,不論正確好壞與否先翻譯出來,留待以后實際應用時再慢慢學習與體會。