首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >如何正确注销事件处理程序

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

Stack Overflow用户
提问于 2008-11-15 17:50:09
回答 2查看 57.2K关注 0票数 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");生成了两条消息,而expected.
  2. Fire("Hello 2");生成了一条消息!

这让我确信,注销works!

  • Fire("Hello 3");new委托抛出了一个NullReferenceException

调试代码显示,注销事件后Firenull

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

我遗漏了什么?

附加问题:根据没有注册事件时Firenull的事实,我得出结论,无论在哪里触发事件,都需要对null进行检查。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2008-11-15 18:00:48

添加事件处理程序的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 { };
票数 85
EN

Stack Overflow用户

发布于 2008-11-15 20:42:29

在触发委托之前,应该始终检查它是否没有目标(它的值为null)。如前所述,一种方法是订阅一个不做任何事情的匿名方法,该方法不会被删除。

public event MyDelegate Fire = delegate {};

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

只需在调用is not threadsafe之前检查委托是否为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 Components)

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

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

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

FireEvent(Fire,"Hello 3");
票数 15
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/292820

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档