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

gyr*_*olf 64 .net c# events delegates

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

 Fire -= new MyDelegate(OnFire);
Run Code Online (Sandbox Code Playgroud)

我认为这不会取消注册事件处理程序,因为它创建了一个之前从未注册过的新委托.但是搜索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);
    }

}
Run Code Online (Sandbox Code Playgroud)

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

  1. Fire("Hello 1"); 按预期产生了两条消息.
  2. Fire("Hello 2");制作了一条消息!
    这使我确信取消注册的new代表工作!
  3. Fire("Hello 3");扔了一个NullReferenceException.
    调试代码显示Firenull注销事件之后.

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

我错过了什么?

其他问题:一个事实,即Firenull在没有注册的事件,我的结论是无处不在的事件被激发,对检查null是必需的.

Bra*_*ger 82

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

Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));
Run Code Online (Sandbox Code Playgroud)

Framework的实现Delegate.Remove不是查看MyDelegate对象本身,而是查看委托引用的方法(Program.OnFire).因此,MyDelegate在取消订阅现有事件处理程序时创建新对象是完全安全的.因此,在添加/删除事件处理程序时,C#编译器允许您使用简写语法(在幕后生成完全相同的代码):您可以省略该new MyDelegate部分:

Fire += OnFire;
Fire -= OnFire;
Run Code Online (Sandbox Code Playgroud)

从事件处理程序中删除最后一个委托时,Delegate.Remove返回null.正如您所知,在提升之前必须检查事件是否为null:

MyDelegate handler = Fire;
if (handler != null)
    handler("Hello 3");
Run Code Online (Sandbox Code Playgroud)

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

public static event MyDelegate Fire = delegate { };
Run Code Online (Sandbox Code Playgroud)

  • 是的.补充:这让我困惑了一段时间,特别是因为在C#中你可以做Fire + =新MyDelegate(OnFire)或Fire + = OnFire; 后者似乎更简单,但只是前者的语法糖. (3认同)

小智 15

在触发委托之前,应始终检查委托是否没有目标(其值为null).如前所述,这样做的一种方法是订阅一个不会被删除的无用的匿名方法.

public event MyDelegate Fire = delegate {};
Run Code Online (Sandbox Code Playgroud)

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

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

public event MyDelegate Fire;
public void FireEvent(string msg)
{
    MyDelegate temp = Fire;
    if (temp != null)
        temp(msg);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

FireEvent(Fire,"Hello 3");
Run Code Online (Sandbox Code Playgroud)

  • 实际上,由于其更强大的内存模型,微软的CLR 2.0无法做到这一点:http://code.logos.com/blog/2008/11/events_and_threads_part_4.html (8认同)