C#中的弱事件

C#中的弱事件(WEAK EVENTS IN C#)

(原创翻译文章·转载请注明来源:http://blog.csdn.net/hulihui)

目录

翻译前序

本文涉及到的.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及以上平台上应用。文章内容深奥难懂,不论正确好坏与否先翻译出来,留待以后实际应用时再慢慢学习与体会。

猜你喜欢

转载自blog.csdn.net/gao271003105/article/details/73089398