詳細解讀C#中的 .NET 弱事件模式
引言
你可能知道,事件處理是內存泄漏的一個常見來源,它由不再使用的對象存留產生,你也許認為它們應該已經被回收了,但不是,并有充分的理由。
在這個短文中(期望如此),我會在 .Net 框架的上下文事件處理中展示這個問題,之后我會教你這個問題的標準解決方案,弱事件模式。有兩種方法,即:
-
“傳統”方法 (嗯,在 .Net 4.5 前,所以也沒那么老),它實現起來比較繁瑣
-
.Net 4.5 框架提供的新方法,它則是盡其可能的簡單
(源代碼在 這里 可供使用。)
從常見事物開始
在一頭扎進本文核心內容前,讓我們回顧一下在代碼中最常使用的兩個事物:類和方法。
事件源
讓我為您介紹一個基本但很有用的事件源類,它***限度地揭示了足夠的復雜性來說明這一點:
- public class EventSource
- {
- public event EventHandlerEvent = delegate { };
- public void Raise()
- {
- Event(this, EventArgs.Empty);
- }
- }
對好奇那個奇怪的空委托初始化方法(delegate { })的人來說,這是一個用來確保事件總被初始化的技巧,這樣就可以不必每次在使用它之前都要檢查它是否不為NULL。
觸發垃圾收集的實用方法
在.net中,垃圾收集以一種不確定的方式觸發。這對我們的實驗很不利,我們的實驗需要以一種確定的方式跟蹤對象的狀態。
所以,我們必須定期觸發自己的垃圾收集操作,同時避免復制管道代碼,管道代碼已經在在一個特定的方法中釋放:
- static void TriggerGC()
- {
- Console.WriteLine("Starting GC.");
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
- Console.WriteLine("GC finished.");
- }
雖然不是很復雜,但是如果你不是很熟悉這種模式,還是有必要小小解釋一下:
-
***個 GC.Collect() 觸發.net的CLR垃圾收集器,對于負責清理不再使用的對象,和那些類中沒有終結器(即c#中的析構函數)的對象,CLR垃圾收集器足夠勝任
-
GC.WaitForPendingFinalizers() 等待其他對象的終結器執行;我們需要這樣做,因為,你將看到我們使用終結器方法去追蹤我們的對象在什么時候被收集的
-
第二個GC.Collect() 確保新生成的對象也被清理了
引入問題
首先讓我們試著通過一些理論,最重要的是還有一個演示的幫助,去了解事件監聽器有哪些問題。
背景
一個對象要想被作為事件偵聽器,需要將其實例方法之一登記為另一個能夠產生事件的對象(即事件源)的事件處理程序,事件源必須保持一個到事件偵聽器對象的引用,以便在事件發生時調用此偵聽器的處理方法。
這很合理,但如果這個引用是一個 強引用,則偵聽器會作為事件源的一個依賴 從而不能作為垃圾回收,即使引用它的***一個對象是事件源。
下面詳細圖解在這下面發生了什么:
事件處理問題
這將不是一個問題,如果你可以控制listener object的生命周期,你可以取消對事件源的訂閱當當你不再需要listener,常常可以使用disposable pattern(用后就扔的模式)。
但是如果你不能在listener生命周期內驗證單點響應,在確定性的方式中你不能把它處理掉,你必須依賴GC處理...這將從不會考慮你所準備的對象,只要事件源還存在著!
例子
理論都是好的,但還是讓我們看看問題和真正的代碼。
這是我們勇敢的時間監聽器,還有點幼稚,我們很快知道為什么:
- public class NaiveEventListener
- {
- private void OnEvent(object source, EventArgs args)
- {
- Console.WriteLine("EventListener received event.");
- }
- public NaiveEventListener(EventSource source)
- {
- source.Event += OnEvent;
- }
- ~NaiveEventListener()
- {
- Console.WriteLine("NaiveEventListener finalized.");
- }
- }
用一個簡單例子來看看怎么實現運作:
- Console.WriteLine("=== Naive listener (bad) ===");
- EventSource source = new EventSource();
- NaiveEventListener listener = new NaiveEventListener(source);
- source.Raise();
- Console.WriteLine("Setting listener to null.");
- listener = null;
- TriggerGC();
- source.Raise();
- Console.WriteLine("Setting source to null.");
- source = null;
- TriggerGC();
輸出:
- EventListener received event.
- Setting listener to null.
- Starting GC.
- GC finished.
- EventListener received event.
- Setting source to null.
- Starting GC.
- NaiveEventListener finalized.
- GC finished.
讓我們分析下這個運作流程:
-
“EventListener received event.“:這是我們調用 “source.Raise()”的結果; perfect, seems like we’re listening.
-
“Setting listener to null.“: 我們把本地事件監聽器對象引用賦空值,這樣應該可以讓垃圾回收器回收了.
-
“Starting GC.“: 垃圾回收開始.
-
“GC finished.“: 垃圾回收開始, 但是 但是我們的事件監聽器沒有被回收器回收, 這樣就證明了事件監聽器的析構函數沒有被調用。
-
“EventListener received event.“: 第二次調用 “source.Raise()”來確認,發現這監聽器還活著。
-
“Setting source to null.“: 我們在賦空值給事件的原對象.
-
“Starting GC.“: 第二次垃圾回收.
-
“NaiveEventListener finalized.“: 這一次幼稚的事件監聽終于被回收了,遲到總好過沒有.
-
“GC finished.“:第二次垃圾回收完成.
結論:確實有一個隱藏的對事件監聽器的強引用,目的是防止它在事件源被回收之前被回收!
希望有針對此問題的標準解決方案:讓事件源可以通過弱引用來引用偵聽器,在事件源存在時也可以回收偵聽器對象。
這里有一個標準的模式及其在.NET框架上的實現:弱事件模式(http://msdn.microsoft.com/en-us/library/aa970850.aspx)。 And there is a standard pattern and its implementation in the .Net framework: the weak event pattern.
#p#
弱事件模式
讓我們看看在.NET中如何應付這個問題,
通常有超過一種方法去做,但是在這種情況下可以直接決定:
-
如果你正在使用 .Net 4.5 ,那么你將從簡單的實現受益
-
另外,你必須依靠一點人為的技巧手段
傳統方式
-
WeakEventManager 是所有模式管道的封裝
-
IWeakEventListener 是管道,它允許一個組件連接到WeakEventManager管件
(這兩個位于WindowBase程序集,你將需要參考你自己的如果你不在開發WPF項目,你應該準確的參考WindowBase)
因此這有兩步處理.
首先通過繼承WeakEventManager來實現一個自定義事件管理器:
-
重寫 StartListening 和 StopListening 方法,分別注冊一個新的handler和注銷一個已存在的; 它們將被WeakEventManager基類使用。
-
提供兩個方法來訪問listener列表, 命名為 “AddListener” 和 “RemoveListener “,給自定義事件管理器的使用者使用。
-
通過在自定義事件管理器上暴露一個靜態屬性,提供一個方式去獲得當前線程的事件管理器。
之后使listenr實現IWeakEventListenr接口:
-
實現 ReceiveWeakEvent 方法
-
嘗試去處理這個事件
-
如果無誤的處理好事件,將返回true
有很多要說的,但是可以相對地轉換成一些代碼:
首先是自定義弱事件管理器:
- public class EventManager : WeakEventManager
- {
- private static EventManager CurrentManager
- {
- get
- {
- EventManager manager = (EventManager)GetCurrentManager(typeof(EventManager));
- if (manager == null)
- {
- manager = new EventManager();
- SetCurrentManager(typeof(EventManager), manager);
- }
- return manager;
- }
- }
- public static void AddListener(EventSource source, IWeakEventListener listener)
- {
- CurrentManager.ProtectedAddListener(source, listener);
- }
- public static void RemoveListener(EventSource source, IWeakEventListener listener)
- {
- CurrentManager.ProtectedRemoveListener(source, listener);
- }
- protected override void StartListening(object source)
- {
- ((EventSource)source).Event += DeliverEvent;
- }
- protected override void StopListening(object source)
- {
- ((EventSource)source).Event -= DeliverEvent;
- }
- }
之后是事件listener:
- public class LegacyWeakEventListener : IWeakEventListener
- {
- private void OnEvent(object source, EventArgs args)
- {
- Console.WriteLine("LegacyWeakEventListener received event.");
- }
- public LegacyWeakEventListener(EventSource source)
- {
- EventManager.AddListener(source, this);
- }
- public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
- {
- OnEvent(sender, e);
- return true;
- }
- ~LegacyWeakEventListener()
- {
- Console.WriteLine("LegacyWeakEventListener finalized.");
- }
- }
檢查下:
- Console.WriteLine("=== Legacy weak listener (better) ===");
- EventSource source = new EventSource();
- LegacyWeakEventListener listener = new LegacyWeakEventListener(source);
- source.Raise();
- Console.WriteLine("Setting listener to null.");
- listener = null;
- TriggerGC();
- source.Raise();
- Console.WriteLine("Setting source to null.");
- source = null;
- TriggerGC();
輸出:
- LegacyWeakEventListener received event.
- Setting listener to null.
- Starting GC.
- LegacyWeakEventListener finalized.
- GC finished.
- Setting source to null.
- Starting GC.
- GC finished.
非常好,它起作用了,我們的事件listener對象現在可以在***次GC里正確的析構,即使事件源對象還存活,不再泄露內存了.
但是要寫一堆代碼就為了一個簡單的listener,想象一下你有一堆這樣的listener,你必須要為每個類型的寫一個弱事件管理器!
如果你很擅長代碼重構,你可以發現一個聰明的方式去重構所有通用的代碼.
在.Net 4.5 出現之前,你必須自己實現弱事件管理器,但是現在,.Net提供一個標準的解決方案來解決這個問題了,現在就來回顧下吧!
.Net 4.5 方式
.Net 4.5 已介紹了一個新的泛型版本的遺留WeakEventManager: WeakEventManager<TEventSource, TEventArgs>.
(這個類可以在WindowsBase集合.)
多虧了 .Net WeakEventManager<TEventSource, TEventArgs> 自己處理泛型, 不用去一個個實現新事件管理器.
而且代碼還簡單和可讀:
- public class WeakEventListener
- {
- private void OnEvent(object source, EventArgs args)
- {
- Console.WriteLine("WeakEventListener received event.");
- }
- public WeakEventListener(EventSource source)
- {
- WeakEventManager.AddHandler(source, "Event", OnEvent);
- }
- ~WeakEventListener()
- {
- Console.WriteLine("WeakEventListener finalized.");
- }
- }
簡單的一行代碼,真簡潔.
其他實現的使用也是相似的, 就是裝入所有東西到事件listener類里:
- Console.WriteLine("=== .Net 4.5 weak listener (best) ===");
- EventSource source = new EventSource();
- WeakEventListener listener = new WeakEventListener(source);
- source.Raise();
- Console.WriteLine("Setting listener to null.");
- listener = null;
- TriggerGC();
- source.Raise();
- Console.WriteLine("Setting source to null.");
- source = null;
- TriggerGC();
輸出也是肯定正確的:
- WeakEventListener received event.
- Setting listener to null.
- Starting GC.
- WeakEventListener finalized.
- GC finished.
- Setting source to null.
- Starting GC.
- GC finished.
預期結果也跟之前一樣,還有什么問題?!
結論
正如你看到的,在.Net上實現弱事件模式 是十分直接, 特別在 .Net 4.5.
如果你沒有用.Net 4.5來實現,將需要一堆代碼, 你可能不去用任何模式而是直接使用C# (+= and -=), 看看是否有內存問題,如果注意到泄露,還需要花必要的時間去實現一個。
但是用 .Net 4.5, 它是自由和簡潔,而且由框架管理, 你可以毫無顧慮的選擇它, 盡管沒有 C# 語法 “+=” 和 “-=” 的酷, 但是語義是清晰的,這才是最重要的.
我已經盡可能的準確的有技術的避免拼寫錯誤,如果你發現有打錯字或錯誤或代碼上的問題或其他問題,可以評論留言哦.
英文原文:The .Net weak event pattern in C#
譯文鏈接:http://www.oschina.net/translate/the-net-weak-event-pattern-in-csharp