使用扩展方法提升C#事件 - 这很糟糕吗?

时间:2008-10-23 21:04:35

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

我们都熟悉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方法,我们是否遗漏了什么? (我注意到的一件事是它不适用于具有显式添加/删除代码的事件。)

6 个答案:

答案 0 :(得分:56)

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

但是,有一种更简单的方法可以使其成为线程安全的 - 使用no-op处理程序初始化它:

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”方法:您是否计划从中派生类?在我看来,大多数课程都应该密封。如果您执行,您是否希望这些派生类能够引发事件?如果这些问题中的任何一个的答案都是“不”,那就不要打扰了。如果两者的答案都是“是”,那么请执行:)

答案 1 :(得分:9)

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

SomethingHappened?.Invoke(this, e);

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

线程问题&#34;处理程序&#34;问题所解决的问题是在这里回避的,因为在该代码中,SomethingHappened只被访问一次,所以在测试和调用之间不可能将它设置为null。

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

答案 2 :(得分:5)

[这是一个想法]

只需按推荐方式编写代码一次,然后完成。那么你不会混淆你的同事看着代码,认为你做错了什么?

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

答案 3 :(得分:3)

代码更少,更易读。我喜欢。

如果您对表现不感兴趣,可以声明您的活动,以避免空检查:

public event EventHandler SomethingHappened = delegate{};

答案 4 :(得分:1)

通过将处理程序分配给局部变量,您不是“确保”线程安全。在分配后,您的方法仍然可能会中断。例如,如果用于监听事件的类在中断期间被处理掉,那么您将在已处理的类中调用方法。

你正在从一个空引用异常中拯救自己,但有更简单的方法可以做到这一点,正如Jon Skeet和cristianlibardo在他们的答案中指出的那样。

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

答案 5 :(得分:0)

要使上述答案更进一步,您可以保护自己免受处理程序之一引发异常的影响。如果发生这种情况,则不会调用后续处理程序。

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

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