如何正确注销一个事件处理程序
在代码审查中,我偶然发现了这个(简化的)代码片段来取消注册一个事件处理程序:
Fire -= new MyDelegate(OnFire);
我以为这不会取消注册事件处理程序,因为它创build了一个以前从未注册过的新代理。 但是searchMSDN我发现了几个使用这个习惯用法的代码示例。
所以我开始了一个实验:
internal class Program { public delegate void MyDelegate(string msg); public static event MyDelegate Fire; private static void Main(string[] args) { Fire += new MyDelegate(OnFire); Fire += new MyDelegate(OnFire); Fire("Hello 1"); Fire -= new MyDelegate(OnFire); Fire("Hello 2"); Fire -= new MyDelegate(OnFire); Fire("Hello 3"); } private static void OnFire(string msg) { Console.WriteLine("OnFire: {0}", msg); } }
令我惊讶的是,发生了以下情况:
-
Fire("Hello 1");
如预期的那样产生了两条消息。 -
Fire("Hello 2");
产生了一个消息!
这使我确信注销new
代表是有效的! -
Fire("Hello 3");
抛出一个NullReferenceException
。
debugging代码显示注销事件后,Fire
为null
。
我知道,对于事件处理程序和委托,编译器会在场景后面生成很多代码。 但我仍然不明白为什么我的推理是错误的。
我错过了什么?
另外的问题:从没有注册事件的情况下, Fire
是null
这一事实,我得出结论:无论事件是否被触发,都需要检查null
。
C#编译器的添加事件处理程序的默认实现调用Delegate.Combine
,同时删除事件处理程序调用Delegate.Remove
:
Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));
Delegate.Remove
的框架的实现不看MyDelegate
对象本身,而是在委托引用的方法( Program.OnFire
)。 因此,在取消订阅现有事件处理程序时,创build新的MyDelegate
对象是完全安全的。 因此,C#编译器允许您在添加/删除事件处理程序时使用简写语法(在后台生成完全相同的代码):您可以省略new MyDelegate
部分:
Fire += OnFire; Fire -= OnFire;
从事件处理程序中删除最后一个委托时, Delegate.Remove
返回null。 正如你发现的那样,在提高它之前检查事件是非常重要的:
MyDelegate handler = Fire; if (handler != null) handler("Hello 3");
它被分配给一个临时的局部variables来抵御可能的竞争条件,在其他线程上取消订阅事件处理程序。 (有关将事件处理程序分配给本地variables的线程安全性的详细信息,请参阅我的博客post 。)另一种防范此问题的方法是创build一个始终订阅的空白委托; 虽然这使用更多的内存,事件处理程序不能为null(和代码可以更简单):
public static event MyDelegate Fire = delegate { };
你应该总是检查一个委托是否没有目标(它的值为空)在开火之前。 如前所述,这样做的一个方法是订阅一个不会被删除的无所作为的匿名方法。
public event MyDelegate Fire = delegate {};
但是,这只是一个黑客来避免NullReferenceExceptions。
只是简单地检查一个委托在调用之前是否为线程安全,因为其他线程可以在空检查之后取消注册并在调用时使其为空。 还有一种解决方法是将委托复制到一个临时variables中:
public event MyDelegate Fire; public void FireEvent(string msg) { MyDelegate temp = Fire; if (temp != null) temp(msg); }
不幸的是,JIT编译器可能会优化代码,消除临时variables,并使用原始代理。 (根据Juval Lowy编程.NET组件)
所以为了避免这个问题,可以使用接受委托作为参数的方法:
[MethodImpl(MethodImplOptions.NoInlining)] public void FireEvent(MyDelegate fire, string msg) { if (fire != null) fire(msg); }
请注意,如果没有MethodImpl(NoInlining)属性,JIT编译器可能会使内联方法变得毫无价值。 由于委托是不可变的,所以这个实现是线程安全的。 你可以使用这个方法:
FireEvent(Fire,"Hello 3");
- .NET框架如何为OutOfMemoryException分配内存?
- 如何创build一个WPF窗口没有可以通过一个手柄只能resize的边框?
- Visual Studio项目的依赖关系图
- 有没有一个“String.Format”,可以接受命名input参数,而不是索引占位符?
- 如何仅使用物理wsdl文件生成服务引用
- 使用MVVMpipe理多个select
- 为什么不应该默认所有的function是asynchronous的?
- 如何强制在Visual Studio 2013中打开解决scheme文件(SLN)?
- Visual Studio立即窗口 – Lambdaexpression式不允许 – 是否有解决方法或替代?