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