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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/292820/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 21:57:26  来源:igfitidea点击:

How to correctly unregister an event handler

c#.neteventsdelegates

提问by gyrolf

In a code review, I stumbled over this (simplified) code fragment to unregister an event handler:

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

 Fire -= new MyDelegate(OnFire);

I thought that this does not unregister the event handler because it creates a new delegate which had never been registered before. But searching MSDN I found several code samples which use this idiom.

我认为这不会取消注册事件处理程序,因为它创建了一个以前从未注册过的新委托。但是搜索 MSDN 我发现了几个使用这个习语的代码示例。

So I started an experiment:

于是我开始了一个实验:

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

}

To my surprise, the following happened:

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

  1. Fire("Hello 1");produced two messages, as expected.
  2. Fire("Hello 2");produced one message!
    This convinced me that unregistering newdelegates works!
  3. Fire("Hello 3");threw a NullReferenceException.
    Debugging the code showed that Fireis nullafter unregistering the event.
  1. Fire("Hello 1");正如预期的那样,产生了两条消息。
  2. Fire("Hello 2");产生了一条消息!
    这使我确信取消注册new代表有效!
  3. Fire("Hello 3");扔了一个NullReferenceException
    调试代码显示这Firenull在取消注册事件之后。

I know that for event handlers and delegate, the compiler generates a lot of code behind the scene. But I still don't understand why my reasoning is wrong.

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

What am I missing?

我错过了什么?

Additional question: from the fact that Fireis nullwhen there are no events registered, I conclude that everywhere an event is fired, a check against nullis required.

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

采纳答案by Bradley Grainger

The C# compiler's default implementation of adding an event handler calls Delegate.Combine, while removing an event handler calls Delegate.Remove:

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

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

The Framework's implementation of Delegate.Removedoesn't look at the MyDelegateobject itself, but at the method the delegate refers to (Program.OnFire). Thus, it's perfectly safe to create a new MyDelegateobject when unsubscribing an existing event handler. Because of this, the C# compiler allows you to use a shorthand syntax (that generates exactly the same code behind the scenes) when adding/removing event handlers: you can omit the new MyDelegatepart:

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

Fire += OnFire;
Fire -= OnFire;

When the last delegate is removed from the event handler, Delegate.Removereturns null. As you have found out, it's essential to check the event against null before raising it:

从事件处理程序中删除最后一个委托时,Delegate.Remove返回 null。正如您所发现的,在引发事件之前检查事件是否为 null 是必不可少的:

MyDelegate handler = Fire;
if (handler != null)
    handler("Hello 3");

It's assigned to a temporary local variable to defend against a possible race condition with unsubscribing event handlers on other threads. (See my blog postfor details on the thread safety of assigning the event handler to a local variable.) Another way to defend against this problem is to create an empty delegate that is always subscribed; while this uses a little more memory, the event handler can never be null (and the code can be simpler):

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

public static event MyDelegate Fire = delegate { };

回答by user37325

You should always check whether a delegate has no targets (its value is null) before firing it. As said before, one way of doing this is to subscribe with a do-nothing anonymous method which won't be removed.

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

public event MyDelegate Fire = delegate {};

However, this is just a hack to avoid NullReferenceExceptions.

然而,这只是一个避免 NullReferenceExceptions 的技巧。

Just simply cheking whether a delegate is null before invoking is not threadsafe as an other thread can deregister after the null-check and making it null when invoking. There is an other solution is to copy the delegate into a temporary variable:

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

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

Unfortunately, the JIT compiler may optimize the code, eliminate the temporary variable, and use the original delegate. (as per Juval Lowy - Programming .NET Components)

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

So to avoid this problem, you could use method which accepts a delegate as parameter:

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

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

Note that without the MethodImpl(NoInlining) attribute the JIT compiler could inline the method making it worthless. Since delegates are immutable this implementation is threadsafe. You could use this method as:

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

FireEvent(Fire,"Hello 3");