譯文:C#中的弱事件(Weak Events in C#)

翻譯前序

 

本文涉及到的.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)回收的監聽對象的引用。sass

 

這類問題存在許多不一樣的解決方法。本文將解釋其中的一些方法,探討它們的優缺點。我將這些方法分爲兩類:首先,咱們假設事件源是一個有正常C#事件的類;而後,咱們容許修改事件源以適應不一樣的方法。安全

 

究竟什麼是事件?

 

許多程序員認爲事件是委託鏈表。這是徹底錯誤的。事實上,委託本身有「多播」(multi-cast)能力:多線程

EventHandler eh = Method1;
 eh += Method2;

那麼,什麼是事件?初步看,它們相似屬性(properties):封裝一個委託字段並限制其訪問。一般狀況下,一個公共委託字段(或公共委託屬性)意味着其它對象能夠清除事件處理程序或激發事件,而咱們只但願事件的定義者具備有這種操做能力。本質上,屬性是一對get/set方法、事件是一對add/remove方法。併發

public event EventHandler MyEvent
 { 
    add {...}
    remove {...}
 }

上述代碼中,只有增長與移除操做是公開的,其它類不能請求執行處理程序鏈表,不能清除鏈表,也不能調用事件。使用這種形式帶來的問題是,C#事件簡寫語法有時引發編程者的困惑:app

public event EventHandler MyEvent;

進一步擴展到下面狀況:ide

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
 {
     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編譯一個轉發器方法(相似前面方案的「轉發代碼」)。

 

優勢:

彷佛是一個直八正的弱事件;幾乎沒有代碼開銷。

 

缺點:

Does not work in partial trust because it uses reflection on private methods.

建議

  • 運行在WPF的UI線程上的任何對象(例如,附加事件到定製控件),使用WeakEventManager;
  • 若是想提供一個弱事件,使用FastSmartWeakEvent;
  • 若是想消費一個事件,使用WeakEventHandler。

 

翻譯後記

 

最近,特別關注.NET上的委託和事件及相關實現技術。瀏覽codeproject時看到一篇關於Weak Events的文章,因好奇這個概念就多讀了幾遍,發現其中的一些構思和方法比較有深度和技巧,也澄清了幾個在事件概念上的誤解和模糊點。該文主要探討.NET 3.0及之後平臺的實現技術。可是其中的基本思想(如:WeakReference)仍是能夠在.NET 2.0及以上平臺上應用。文章內容深奧難懂,不論正確好壞與否先翻譯出來,留待之後實際應用時再慢慢學習與體會。

 

第一次翻譯技術文章,加之對.NET 3.0/3.5/4.0的相關技術認識不深,譯文中的不當或錯誤之處請讀者指正。

相關文章
相關標籤/搜索