如何正确取消注册事件处理程序

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

 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
    调试代码显示注销事件后Firenull
  • 我知道,对于事件处理程序和委托,编译器会在场景后面生成大量代码。 但我仍然不明白为什么我的推理是错误的。

    我错过了什么?

    额外的问题:当没有事件注册时, Firenull ,我得出结论,无论事件是否被触发,都需要检查null


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

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

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

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

    public event MyDelegate Fire = delegate {};
    

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

    只是在调用之前简单地检查一个委托是否为空不是线程安全的,因为其他线程可以在空检查之后取消注册并在调用时将其置为空。 还有一种解决方法是将委托复制到临时变量中:

    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");
    
    链接地址: http://www.djcxy.com/p/51429.html

    上一篇: How to correctly unregister an event handler

    下一篇: Event handling with an anonymous delegate