【问题标题】:How to correctly unregister an event handler如何正确注销事件处理程序
【发布时间】:2010-09-22 12:16:30
【问题描述】:

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

 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"); 产生了一条消息!
    这让我确信取消注册 new 代表是可行的!
  3. Fire("Hello 3"); 扔了一个NullReferenceException
    调试代码显示注销事件后Firenull

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

我错过了什么?

附加问题:根据Firenull 的事实,当没有注册事件时,我得出结论,无论何时触发事件,都需要检查null

【问题讨论】:

    标签: c# .net events delegates


    【解决方案1】:

    添加事件处理程序的 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");
    

    它被分配给一个临时局部变量,以防止可能的竞争条件与取消订阅其他线程上的事件处理程序。 (有关将事件处理程序分配给局部变量的线程安全性的详细信息,请参阅my blog post。)解决此问题的另一种方法是创建一个始终订阅的空委托;虽然这会使用更多内存,但事件处理程序永远不能为空(代码可以更简单):

    public static event MyDelegate Fire = delegate { };
    

    【讨论】:

    • 是的。补充一下:这也让我困惑了一段时间,特别是因为在 C# 中你可以做 Fire += new MyDelegate(OnFire) 或 Fire += OnFire;后者似乎更简单,但只是前者的语法糖。
    • @Nicholas Piasecki:谢谢;我更新了我的答案,指出(相当有用的)速记。
    • 可能值得注意的是,如果该多播委托中的任何方法也单独订阅和取消订阅,则添加多播委托并稍后取消订阅可能会产生不正确的结果。
    【解决方案2】:

    在触发委托之前,您应该始终检查委托是否没有目标(其值为空)。 如前所述,一种方法是订阅一个不会被删除的无操作匿名方法。

    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 - Programming .NET Components)

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

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

    请注意,如果没有 MethodImpl(NoInlining) 属性,JIT 编译器可能会内联该方法,使其毫无价值。 因为委托是不可变的,所以这个实现是线程安全的。 您可以将此方法用作:

    FireEvent(Fire,"Hello 3");
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-21
    • 1970-01-01
    • 2019-01-31
    • 2015-11-24
    • 2012-04-20
    相关资源
    最近更新 更多