【问题标题】:Raising C# events with an extension method - is it bad?使用扩展方法引发 C# 事件 - 这很糟糕吗?
【发布时间】:2010-09-18 21:26:25
【问题描述】:

我们都熟悉 C# 事件声明的可怕之处。为确保线程安全,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);
}

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

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

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

public event EventHandler SomethingHappened;

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

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

【问题讨论】:

标签: c# .net events event-handling extension-methods


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

    SomethingHappened?.Invoke(this, e);
    

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

    问题中的“处理程序”代码要解决的线程问题在这里被回避了,因为就像在那个代码中一样,SomethingHappened 只被访问一次,所以它不可能在 test 和调用。

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

    【讨论】:

    • 是的,这就是你现在应该使用的。
    【解决方案3】:

    [这是一个想法]

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

    [与编写事件处理程序相比,我阅读了更多试图找到编写事件处理程序的方法的帖子。]

    【讨论】:

      【解决方案4】:

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

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

      public event EventHandler SomethingHappened = delegate {};
      

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

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

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

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

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

      【讨论】:

      • 好点;一旦您进入该方法,@event 就无法更改。我知道你可以订阅一个空的委托,但我更关心的是放弃 On 方法是好是坏。
      • 我从您的优秀书籍中认识到使用委托的建议{}!这太棒了:)
      • 你们需要将 [MethodImpl(MethodImplOptions.NoInlining)] 属性添加到这些扩展方法中,否则 JITter 可能会优化您将委托复制到临时变量的尝试,从而允许空引用异常。这可能出现在 Kyralessa 和 Jon Skeet 的版本中。因此,只需添加 [MethodImpl(MethodImplOptions.NoInlining)] 就可以了,而且,是的,您不再需要显式复制到临时变量,因为只需通过方法的参数传入值即可完成此操作。
      • @Mike:不,我不相信 JITter is 允许这样做。不久前有人提出了这个问题,但我和某人谈过(我不记得是乔·达菲还是万斯·莫里森;像那样的人)说这完全违反了 JITter 所允许的规则和内存模型。
      • @Ghopper21:不,在这种情况下,参数实际上充当局部变量 - 事件处理程序的任何机会都不会在 RaiseEvent 中可见,因为 @event 只是一个参数...一个正常的局部变量。调用该方法时,该字段的当前值将被复制为参数的值...并且对该字段的进一步更改不会影响它。
      【解决方案5】:

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

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

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

      【讨论】:

      • 我认为他们的意思是“避免竞争条件”。
      【解决方案6】:

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

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

      public event EventHandler SomethingHappened = delegate{};
      

      【讨论】:

        猜你喜欢
        • 2015-09-02
        • 2011-10-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-13
        • 1970-01-01
        相关资源
        最近更新 更多