代码之家  ›  专栏  ›  技术社区  ›  Ryan Lundy

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

  •  49
  • Ryan Lundy  · 技术社区  · 16 年前

    我们都熟悉C#event declaration的恐怖。为确保螺纹安全, 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);
    }
    

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

    5 回复  |  直到 5 年前
        1
  •  59
  •   Jon Skeet    16 年前

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

    但是,有一种更简单的方法使其线程安全—使用无操作处理程序初始化它:

    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”方法:您是否计划从中派生类?在我看来,大多数类都应该被密封。如果你

        2
  •  14
  •   Bob Sammers 15ee8f99-57ff-4f92-890c-b56153    9 年前

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

    SomethingHappened?.Invoke(this, e);
    

    Invoke() 仅当代理已为事件注册(即,它不是null)时才调用,这要感谢null条件运算符“?”。

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

    这个答案也许与最初的问题相去甚远,但对于那些寻找更简单的方法来引发事件的人来说却非常相关。

        3
  •  5
  •   Robert Paulson    16 年前

    [这里有一个想法]

    只需按照推荐的方式编写代码一次,然后就可以完成了。这样你就不会让你的同事在查看代码时觉得你做错了什么?

        4
  •  3
  •   Cristian Libardo    16 年前

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

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

    public event EventHandler SomethingHappened = delegate{};
    
        5
  •  1
  •   Isak Savo    16 年前

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

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

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

        6
  •  0
  •   Bill Tarbell    5 年前

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

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