使用扩展方法引发 C# 事件 - 是不是很糟糕?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/231525/
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 19:08:12  来源:igfitidea点击:

Raising C# events with an extension method - is it bad?

c#.neteventsevent-handlingextension-methods

提问by Ryan Lundy

We're all familiar with the horror that is C# event declaration. To ensure thread-safety, the standard is to write something like this:

我们都熟悉 C# 事件声明的可怕之处。为了确保线程安全,标准是这样写

public event EventHandler SomethingHappened;
protected virtual void OnSomethingHappened(EventArgs e)
{            
    var handler = SomethingHappened;
    if (handler != null)
        handler(this, e);
}

Recently in some other question on this board (which I can't find now), someone pointed out that extension methods could be used nicely in this scenario. Here's one way to do it:

最近在这个板上的其他一些问题(我现在找不到)中,有人指出在这种情况下可以很好地使用扩展方法。这是一种方法:

static public class EventExtensions
{
    static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
    {
        var handler = @event;
        if (handler != null)
            handler(sender, e);
    }
    static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
        where T : EventArgs
    {
        var handler = @event;
        if (handler != null)
            handler(sender, e);
    }
}

With these extension methods in place, all you need to declare and raise an event is something like this:

有了这些扩展方法,你需要声明和引发一个事件是这样的:

public event EventHandler SomethingHappened;

void SomeMethod()
{
    this.SomethingHappened.RaiseEvent(this, EventArgs.Empty);
}

My question: Is this a good idea? Are we missing anything by not having the standard On method? (One thing I notice is that it doesn't work with events that have explicit add/remove code.)

我的问题:这是个好主意吗?我们是否因为没有标准的 On 方法而遗漏了什么?(我注意到的一件事是它不适用于具有显式添加/删除代码的事件。)

采纳答案by Jon Skeet

It will still work with events that have an explicit add/remove - you just need to use the delegate variable (or however you've stored the delegate) instead of the event name.

它仍然适用于具有显式添加/删除的事件 - 您只需要使用委托变量(或者您存储委托的方式)而不是事件名称。

However, there's an easier way to make it thread-safe - initialize it with a no-op handler:

但是,有一种更简单的方法可以使其成为线程安全的 - 使用无操作处理程序对其进行初始化:

public event EventHandler SomethingHappened = delegate {};

The performance hit of calling an extra delegate will be negligible, and it sure makes the code easier.

调用额外委托对性能的影响可以忽略不计,它确实使代码更容易。

By the way, in your extension method you don't need an extra local variable - you could just do:

顺便说一句,在您的扩展方法中,您不需要额外的局部变量 - 您可以这样做:

static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
{
    if (@event != null)
        @event(sender, e);
}

static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
    where T : EventArgs
{
    if (@event != null)
        @event(sender, e);
}

Personally I wouldn't use a keyword as a parameter name, but it doesn't really change the calling side at all, so do what you want :)

就我个人而言,我不会使用关键字作为参数名称,但它根本不会真正改变调用方,所以做你想做的:)

EDIT: As for the "OnXXX" method: are you planning on your classes being derived from? In my view, most classes should be sealed. If you do, do you want those derived classes to be able to raise the event? If the answer to either of these questions is "no" then don't bother. If the answer to both is "yes" then do :)

编辑:至于“OnXXX”方法:您是否计划派生自您的类?在我看来,大多数类应该是密封的。如果这样做,您是否希望这些派生类能够引发事件?如果这些问题中的任何一个的答案是“否”,那么请不要打扰。如果两者的答案都是“是”,那么请执行 :)

回答by Cristian Libardo

Less code, more readable. Me like.

代码更少,可读性更强。我喜欢。

If you're not interested in performance you can declare your event like this to avoid the null check:

如果您对性能不感兴趣,您可以像这样声明您的事件以避免空检查:

public event EventHandler SomethingHappened = delegate{};

回答by Isak Savo

You're not "ensuring"thread safety by assigning the handler to a local variable. Your method could still be interrupted after the assignment. If for example the class that used to listen for the event gets disposed during the interruption, you're calling a method in a disposed class.

通过将处理程序分配给局部变量,您并没有“确保”线程安全。您的方法在分配后仍可能被中断。例如,如果用于侦听事件的类在中断期间被释放,则您正在调用已释放类中的方法。

You're saving yourself from a null reference exception, but there are easier ways to do that, as Jon Skeet and cristianlibardo pointed out in their answers.

您正在避免空引用异常,但有更简单的方法可以做到这一点,正如 Jon Skeet 和 cristianlibardo 在他们的回答中指出的那样。

Another thing is that for non-sealed classes, the OnFoo method should be virtual which I don't think is possible with extension methods.

另一件事是,对于非密封类, OnFoo 方法应该是虚拟的,我认为扩展方法是不可能的。

回答by Robert Paulson

[Here's a thought]

[这里有一个想法]

Just write the code once in the recommended way and be done with it. Then you won't confuse your colleagues looking over the code thinking you did something wrong?

只需按照推荐的方式编写一次代码即可完成。那么您不会让查看代码的同事认为您做错了什么吗?

[I read more posts trying to find ways around writing an event handler than I ever spend writing an event handler.]

[我阅读了更多试图找到编写事件处理程序的方法的帖子,而不是我花在编写事件处理程序上的时间。]

回答by Bob Sammers

Now C# 6 is here, there is a more compact, thread-safe way to fire an event:

现在 C# 6 来了,有一种更紧凑、线程安全的方式来触发事件:

SomethingHappened?.Invoke(this, e);

Invoke()is only called if delegates are registered for the event (i.e. it's not null), thanks to the null-conditional operator, "?".

Invoke()仅当为事件注册了委托(即它不为空)时才会调用,这要归功于空条件运算符“?”。

The threading problem the "handler" code in the question sets out to solve is sidestepped here because, like in that code, SomethingHappenedis only accessed once, so there is no possibility of it being set to null between test and invocation.

问题中的“处理程序”代码要解决的线程问题在这里被回避了,因为就像在该代码中一样,SomethingHappened只访问一次,因此在测试和调用之间不可能将其设置为 null。

This answer is perhaps tangential to the original question, but very relevent for those looking for a simpler method to raise events.

这个答案可能与原始问题无关,但对于那些寻找更简单的方法来引发事件的人来说非常相关。

回答by Bill Tarbell

To take the above answers a step further you could protect yourself against one of your handlers throwing an exception. If this were to happen then the subsequent handlers wouldn't be called.

为了进一步回答上述答案,您可以保护自己免受其中一个处理程序抛出异常的影响。如果发生这种情况,则不会调用后续处理程序。

Likewise, you could taskify the handlers to prevent a long-running handler from causing an excessive delay for the latter handlers to be informed. This can also protect the source thread from being hiHymaned by a long-running handler.

同样,您可以对处理程序进行任务分配,以防止长时间运行的处理程序导致后一个处理程序被通知的过度延迟。这也可以保护源线程不被长时间运行的处理程序劫持。

  public static class EventHandlerExtensions
  {
    private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

    public static void Taskify(this EventHandler theEvent, object sender, EventArgs args)
    {
      Invoke(theEvent, sender, args, true);
    }

    public static void Taskify<T>(this EventHandler<T> theEvent, object sender, T args)
    {
      Invoke(theEvent, sender, args, true);
    }

    public static void InvokeSafely(this EventHandler theEvent, object sender, EventArgs args)
    {
      Invoke(theEvent, sender, args, false);
    }

    public static void InvokeSafely<T>(this EventHandler<T> theEvent, object sender, T args)
    {
      Invoke(theEvent, sender, args, false);
    }

    private static void Invoke(this EventHandler theEvent, object sender, EventArgs args, bool taskify)
    {
      if (theEvent == null)
        return;

      foreach (EventHandler handler in theEvent.GetInvocationList())
      {
        var action = new Action(() =>
        {
          try
          {
            handler(sender, args);
          }
          catch (Exception ex)
          {
            _log.Error(ex);
          }
        });

        if (taskify)
          Task.Run(action);
        else
          action();
      }
    }

    private static void Invoke<T>(this EventHandler<T> theEvent, object sender, T args, bool taskify)
    {
      if (theEvent == null)
        return;

      foreach (EventHandler<T> handler in theEvent.GetInvocationList())
      {
        var action = new Action(() =>
        {
          try
          {
            handler(sender, args);
          }
          catch (Exception ex)
          {
            _log.Error(ex);
          }
        });

        if (taskify)
          Task.Run(action);
        else
          action();
      }
    }
  }