本文涉及到的.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);于是,我们不得不区分:线程安全的事件、非线程安全的事件。
在这一部分中假设事件是一个正常的C#事件(强引用事件处理程序),且任何清理工作都在监听方完成。
void RegisterEvent() { eventSource.Event += OnEvent; } void DeregisterEvent() { eventSource.Event -= OnEvent } void OnEvent(object sender, EventArgs e) { ... }上面就是我们经常用到的简单有效的形式。然而,当对象不再使用时,通常不能确保DeregisterEvent方法被调用。可以尝试用Dispose模式(它通常意味着非托管资源),但终结器(Finalizer)不会被执行:垃圾收集器不会调用这个终结器,因为事件源仍然保持了监听对象的引用!
void RegisterEvent() { eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { if (!InUse) { eventSource.Event -= OnEvent; return; } ... }现在,不需要有人指出何时监听者不再使用:事件调用时它只需要检查自己即可。然而,如果我们不能使用解决方案0,那么通常情况下也无法从监听对象中确定InUse。假如你正在阅读本文,您可能已经遇到过其中的一个情形了。
但是,比较解决方案0,这个“解决方案”已经有一个严重的缺点了:如果事件是从未激发(即OnEvent从未被调用——译者注),那么也将泄漏监听对象。想象这种情况,许多对象注册到一个静态“SettingsChanged”事件上——所有这些对象将不能被垃圾回收,直到一个设置改变——在程序的生存期内这种设置改变或许永远不会发生。
这个解决方案几乎等同于前一个,区别在于:我们把事件处理代码移到一个包装器类中,该包装器类转发调用到一个弱引用(有关弱引用的概念请参考 (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; } }
请注意,上述方案中储存了一个EventWrapper引用,并有一个公有方法Deregister,可以给监听者增加一个终结器(Finalizer),它可以调用包装器的注销方法。
~ListenerObject() { ew.Deregister(); }这个方案顾全了内存泄漏问题,但是有代价的:对垃圾回收器而言,可终结对象是高代价的。当没有监听对象引用时(除弱引用外),它将在第一次垃圾回收时生存下来并升级一代。假设终结器运行,在接下来的第二次垃圾收集时被回收(新一代的对象)。此外,终结器运行在终结器线程上,如果注册/注销事件的事件源不是线程安全的,也可能引发问题。请记住,C#编译器产生的默认事件不是线程安全的!
下载代码中包含一个可重复使用的包装器类(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完成。
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 ) 。
这里将探讨修改事件源实现弱事件的各种方法。对比监听方的弱事件,所有这些方法都有一个共同的优点:可以较容易地进行线程安全的注册/注销事件处理程序。
本节还得提及WeakEventManager:作为包装器,它附加(“listening-side”)到正常C#事件,也提供(“source-side”)一个弱事件给客户端。WeakEventManager中定义IWeakEventListener接口,监听对象实现接口,事件源只需拥有一个监听者弱引用并调用接口方法即可。
这是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给出了一个事件实现例子,它是线程安全的,当增加另一个处理程序时清除处理程序链表。
WeakEventManager采用了解决方案0,而本解决方案采用了WeakEventHandler包装器:注册一个(object,ForwarderDelegate)对:
eventSource.AddHandler(this, eventSource.AddHandler (me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));
下载代码的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(...);
功能和使用与SmartWeakEvent相同,但显著改善了性能。下面是有两个注册委托(一个实例的方法和一个静态方法)的事件的测试结果:
Normal (strong) event... 16948785 调用每秒 Smart weak event... 91960 调用每秒 Fast smart weak event... 4901840 调用每秒如何工作?不再使用反射调用方法,而在运行时使用System.Reflection.Emit.DynamicMethod编译一个转发器方法(类似前面方案的“转发代码”)。
最近,特别关注.NET上的委托和事件及相关实现技术。浏览codeproject时看到一篇关于Weak Events的文章,因好奇这个概念就多读了几遍,发现其中的一些构思和方法比较有深度和技巧,也澄清了几个在事件概念上的误解和模糊点。该文主要探讨.NET 3.0及以后平台的实现技术。但是其中的基本思想(如:WeakReference)还是可以在.NET 2.0及以上平台上应用。文章内容深奥难懂,不论正确好坏与否先翻译出来,留待以后实际应用时再慢慢学习与体会。
第一次翻译技术文章,加之对.NET 3.0/3.5/4.0的相关技术认识不深,译文中的不当或错误之处请读者指正。