zoukankan      html  css  js  c++  java
  • 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及以上平台上应用。文章内容深奥难懂,不论正确好坏与否先翻译出来,留待以后实际应用时再慢慢学习与体会。

  • 相关阅读:
    第二季-专题11-世界一下变大了-MMU
    第二季-专题10-C语言环境初始化
    第二季-专题9--代码搬移不可少
    第二季-专题8-不用内存怎么行
    第二季-专题6-点亮指路灯
    第二季-专题7-ARM跑快了---时钟初始化
    第二季-专题5-核心初始化
    第二季-专题4-我是bootloader设计师
    消除苹果系统对边框的优化
    css3
  • 原文地址:https://www.cnblogs.com/qing208/p/6720276.html
Copyright © 2011-2022 走看看