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

时间:2022-06-04 08:11:34

翻译前序

本文涉及到的.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)
{
...
}