50

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

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

4

6 回答 6

59

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

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

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”方法:你打算派生你的类吗?在我看来,大多数课程都应该是密封的。如果这样做,您是否希望这些派生类能够引发事件?如果这两个问题的答案是“否”,那么不要打扰。如果两者的答案都是“是”,那么做:)

于 2008-10-23T21:09:04.513 回答
15

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

SomethingHappened?.Invoke(this, e);

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

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

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

于 2015-08-14T17:03:49.477 回答
5

[这里有一个想法]

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

[我阅读的文章试图找到编写事件处理程序的方法比我编写事件处理程序所花费的更多。]

于 2008-10-23T21:49:35.260 回答
3

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

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

public event EventHandler SomethingHappened = delegate{};
于 2008-10-23T21:11:15.993 回答
1

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

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

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

于 2008-10-23T21:16:52.113 回答
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();
      }
    }
  }
于 2019-07-18T13:05:15.687 回答