[英]How to correctly unregister an event handler
在代码审查中,我偶然发现了这个(简化的)代码片段以取消注册事件处理程序:
Fire -= new MyDelegate(OnFire);
我认为这不会取消注册事件处理程序,因为它创建了一个之前从未注册过的新委托。 但是搜索MSDN我发现了几个使用这个习惯用法的代码示例。
所以我开始了一个实验:
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
。 Fire
为null
。 我知道对于事件处理程序和委托,编译器会在场景后面生成大量代码。 但我仍然不明白为什么我的推理是错误的。
我错过了什么?
附加问题:当没有注册事件时Fire
为null
这一事实我得出结论,在触发事件的每个地方都需要对null
进行检查。
C#编译器添加事件处理程序的默认实现调用Delegate.Combine
,同时删除事件处理程序调用Delegate.Remove
:
Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));
Framework的Delegate.Remove
实现不会查看MyDelegate
对象本身,而是查看委托引用的方法( Program.OnFire
)。 因此,在取消订阅现有事件处理程序时创建新的MyDelegate
对象是完全安全的。 因此,在添加/删除事件处理程序时,C#编译器允许您使用简写语法(在幕后生成完全相同的代码):您可以省略new MyDelegate
部分:
Fire += OnFire;
Fire -= OnFire;
从事件处理程序中删除最后一个委托时, Delegate.Remove
返回null。 正如您所知,在提升之前必须检查事件是否为null:
MyDelegate handler = Fire;
if (handler != null)
handler("Hello 3");
它被分配给一个临时局部变量,以防止在其他线程上取消订阅事件处理程序的可能的竞争条件。 (有关将事件处理程序分配给局部变量的线程安全性的详细信息,请参阅我的博客文章 。)防止此问题的另一种方法是创建一个始终订阅的空委托; 虽然这使用了更多的内存,但事件处理程序永远不能为null(并且代码可以更简单):
public static event MyDelegate Fire = delegate { };
在触发委托之前,应始终检查委托是否没有目标(其值为null)。 如前所述,这样做的一种方法是订阅一个不会被删除的无用的匿名方法。
public event MyDelegate Fire = delegate {};
但是,这只是一个避免NullReferenceExceptions的黑客攻击。
只是简单地在调用之前检查委托是否为空而不是线程安全,因为其他线程可以在空检查之后取消注册并在调用时使其为null。 还有一种解决方案是将委托复制到临时变量中:
public event MyDelegate Fire;
public void FireEvent(string msg)
{
MyDelegate temp = Fire;
if (temp != null)
temp(msg);
}
不幸的是,JIT编译器可能会优化代码,消除临时变量,并使用原始委托。 (根据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");
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.