C#编程之C#中的弱事件(Weak Events in C#)
小标 2019-03-13 来源 : 阅读 1249 评论 0

摘要:本文主要向大家介绍了C#编程之C#中的弱事件(Weak Events in C#),通过具体的内容向大家展示,希望对大家学习C#编程有所帮助。

本文主要向大家介绍了C#编程之C#中的弱事件(Weak Events in C#),通过具体的内容向大家展示,希望对大家学习C#编程有所帮助。

C#编程之C#中的弱事件(Weak Events in C#)

原文:Weak Events In C#: Different approaches to weak events. by Daniel Grunwald.
 Download source code - 15.5 KB
翻译前序
翻译后记

目录

引言
究竟什么是事件?
第1部分:监听方(Listener-side)的弱事件

解决方案0:仅仅注销
解决方案1:事件调用后注销
解决方案2:带弱引用(WeakReference)的包装器
解决方案3:终结器(Finalizer)中注销
解决方案4:可重复使用的包装器
解决方案5:弱事件管理器(WeakEventManager)

第2部分:事件源(Source-side)的弱事件

解决方案0:接口
解决方案1:弱引用委托
解决方案2:对象+转发器(Forwarder)
解决方案3:智能弱事件(SmartWeakEvent)
解决方案4:快速智能弱事件(FastSmartWeakEvent)

建议

 
翻译前序
 
本文涉及到的.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做得得心应手时,我们也许想自动完成这项工作,特别是有许多不同的委托类型时。这可以在编译时使用代码生成,或在运行时使用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编译一个转发器方法(类似前面方案的“转发代码”)。
 
优点:
似乎是一个真正的弱事件;几乎没有代码开销。
 
缺点:
-
建议

运行在WPF的UI线程上的任何对象(例如,附加事件到定制控件),使用WeakEventManager;
如果想提供一个弱事件,使用FastSmartWeakEvent;
如果想消费一个事件,使用WeakEventHandler。

 
翻译后记
 
最近,特别关注.NET上的委托和事件及相关实现技术。浏览codeproject时看到一篇关于Weak Events的文章,因好奇这个概念就多读了几遍,发现其中的一些构思和方法比较有深度和技巧,也澄清了几个在事件概念上的误解和模糊点。该文主要探讨.NET 3.0及以后平台的实现技术。但是其中的基本思想(如:WeakReference)还是可以在.NET 2.0及以上平台上应用。文章内容深奥难懂,不论正确好坏与否先翻译出来,留待以后实际应用时再慢慢学习与体会。

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标编程语言C#.NET频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程