如何正确注销事件处理程序?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (39)

在代码审查中,我偶然发现了这个(简化的)代码片段来取消注册一个事件处理程序:

 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);
    }

}

令我惊讶的是,发生了以下情况:

  1. Fire("Hello 1"); 如预期的那样产生了两条消息。
  2. Fire("Hello 2");产生了一条消息!
  3. Fire("Hello 3");扔了一个NullReferenceException

我知道,对于事件处理程序和委托,编译器会在场景后面生成大量代码。但我仍然不明白为什么我的推理是错误的。

我错过了什么?

提问于
用户回答回答于

C#编译器的添加事件处理程序调用的默认实现Delegate.Combine,同时删除事件处理程序调用Delegate.Remove

Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));

框架的实现Delegate.Remove不会看MyDelegate对象本身,而是看代理引用的方法(Program.OnFire)。正因为如此,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 { };
用户回答回答于

你应该始终检查委托在启动之前是否没有目标(其值为空)。如前所述,这样做的一种方式是订阅一个不会被删除的不做任何匿名方法。

public event MyDelegate Fire = delegate {};

但是,这只是一个避免NullReferenceExceptions的黑客攻击。

还有一种解决方法是将委托复制到临时变量中:

public event MyDelegate Fire;
public void FireEvent(string msg)
{
    MyDelegate temp = Fire;
    if (temp != null)
        temp(msg);
}

不幸的是,JIT编译器可能会优化代码,消除临时变量,并使用原始代理。

所以为了避免这个问题,你可以使用接受委托作为参数的方法:

[MethodImpl(MethodImplOptions.NoInlining)]
public void FireEvent(MyDelegate fire, string msg)
{
    if (fire != null)
        fire(msg);
}

请注意,如果没有MethodImpl(NoInlining)属性,JIT编译器可能会使内联方法变得毫无价值。你可以使用这个方法:

FireEvent(Fire,"Hello 3");

所属标签

可能回答问题的人

  • 找虫虫

    0 粉丝0 提问6 回答
  • 爸爸

    腾讯 · 客户端安全 (已认证)

    4 粉丝4 提问5 回答
  • 优惠活动秘书

    0 粉丝2 提问4 回答
  • 人生的旅途

    10 粉丝484 提问4 回答

扫码关注云+社区

领取腾讯云代金券