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