简体   繁体   English

使用扩展方法引发C#事件-不好吗?

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

We're all familiar with the horror that is C# event declaration. 我们都熟悉C#事件声明的恐怖。 To ensure thread-safety, the standard is to write something like this : 为了确保线程安全, 标准是编写类似以下内容的代码

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? 我们是否没有标准的On方法会丢失任何东西? (One thing I notice is that it doesn't work with events that have explicit add/remove code.) (我注意到的一件事是,它不适用于具有显式添加/删除代码的事件。)

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? 编辑:至于“ OnXXX”方法:您打算从中派生您的类吗? 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 :) 如果两者的答案均为“是”,请执行:)

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 (ie 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, SomethingHappened is 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. 这个答案也许与原始问题相切,但是对于那些寻求一种简单方法引发事件的人来说却是非常不切实际的。

[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.] [与编写事件处理程序相比,我阅读了更多试图找到编写事件处理程序的方法的文章。]

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

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. 您可以避免使用null引用异常,但正如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方法应该是虚拟的,我认为扩展方法是不可能的。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM