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


67

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

 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是必需的。

Answers:


85

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

3
是的 补充:这也让我有些困惑,特别是因为在C#中,您可以执行Fire + = new MyDelegate(OnFire)或Fire + = OnFire;后者似乎更简单,但对于前者来说只是语法糖。
尼古拉斯·皮亚塞斯基

@Nicholas Piasecki:谢谢;我更新了答案,以注意该速记(​​非常有用)。
Bradley Grainger

可能值得注意的是,如果该多播委托中的任何方法也分别被订阅和取消订阅,则添加多播委托并随后取消订阅可能会产生错误的结果。
2014年

15

在激发它之前,您应始终检查委托是否没有目标(其值为null)。如前所述,执行此操作的一种方法是使用不删除的匿名方法进行订阅。

public event MyDelegate Fire = delegate {};

但是,这只是避免NullReferenceExceptions的一种手段。

只是简单地在调用之前检查委托是否为null并不是线程安全的,因为其他线程可以在执行null检查后注销并在调用时使其为null。还有另一种解决方案是将委托复制到一个临时变量中:

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

不幸的是,JIT编译器可能会优化代码,消除临时变量并使用原始委托。(根据Juval Lowy-Programming .NET组件)

因此,为避免出现此问题,您可以使用接受委托作为参数的方法:

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

请注意,如果没有MethodImpl(NoInlining)属性,JIT编译器可能会内联该方法,从而使其一文不值。由于委托是不可变的,因此此实现是线程安全的。您可以将这种方法用作:

FireEvent(Fire,"Hello 3");

非常感谢您澄清了JIT问题。到处都有陷阱和陷阱。
gyrolf

8
实际上,由于Microsoft的CLR 2.0具有更强的内存模型,因此这是不可能的:code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.