自动生成空的C#事件处理程序

74

在C#中,不可能触发一个没有处理程序的事件。因此,在每次调用前都需要检查事件是否为null。

if ( MyEvent != null ) {
  MyEvent( param1, param2 );
}

我希望尽可能保持我的代码整洁,摆脱那些空值检查。我认为这不会对性能产生太大影响,至少在我的情况下不会。

MyEvent( param1, param2 );

我现在通过手动为每个事件添加一个空的内联处理程序来解决这个问题。这种方法容易出错,因为我需要记得这样做等等。

void Initialize() {
  MyEvent += new MyEvent( (p1,p2) => { } );
}

是否有一种使用反射和一些CLR魔法自动生成给定类的所有事件的空处理程序的方法?


被接受的答案中的技巧将避免检查 null,但不会确保线程安全。请参见此处:https://dev59.com/GUjSa4cB1Zd3GeqPFXA_#1131204 - user316976
8个回答

162

我在另一篇文章中看到了这个技巧,然后不要脸地将它藏在了我的代码里:

public delegate void MyClickHandler(object sender, string myValue);
public event MyClickHandler Click = delegate {}; // add empty delegate!

//Let you do this:
public void DoSomething() {
    Click(this, "foo");
}

//Instead of this:
public void DoSomething() {
    if (Click != null) // Unnecessary!
        Click(this, "foo");
}

* 如果有人知道这个技巧的来源,请在评论中发布。我真的相信应该给予原始来源应有的荣誉。

(编辑:我从这篇帖子中获得了它 C#的隐藏功能?)


4
就在那儿加上一个空委托吧!这比我原本期望的还要好。谢谢!我现在就去看“隐藏特性”文章了。 - Tomas Andrle
1
是的 - 那篇帖子是无价的!一定要经常投票。他们为我们所有人做了很大的贡献。 - Dinah
7
这个技术之所以得到-1分,有两个原因:1)它会导致运行时性能和内存开销;2)这种技术容易出错,特别是与下面描述的扩展方法相比。仅仅查看调用点并不足以确定该方法的正确性,但是该扩展方法适用于所有事件,无论该事件是否使用空委托初始化。 - Sam Harwell
值得注意的是,使用空委托进行初始化仅适用于“类”,而不适用于“结构体”。 - sebrockm

61

符号表示:

if ( MyEvent != null ) {
  MyEvent( param1, param2 );
}

不是线程安全的。您应该这样做:

EventHandler handler = this.MyEvent;
if ( null != handler ) { handler( param1, param2 ); }

我知道这有些麻烦,因此您可以使用助手方法:

static void RaiseEvent( EventHandler handler, object sender, EventArgs e ) {
    if ( null != handler ) { handler( sender, e ); }
}

然后调用:

RaiseEvent( MyEvent, param1, param2 );

如果你正在使用C# 3.0,你可以将帮助方法声明为扩展方法:

static void Raise( this EventHandler handler, object sender, EventArgs e ) {
    if ( null != handler ) { handler( sender, e ); }
}

然后调用:

MyEvent.Raise( param1, param2 );

同时,您可以为其他事件处理程序创建下一个扩展/帮助方法。例如:

static void Raise<TEventArgs>( this EventHandler<TEventArgs> handler,
    object sender, TEventArgs e ) where TEventArgs : EventArgs
{
    if ( null != handler ) { handler( sender, e ); }
}

5
使用扩展方法是一个漂亮的解决方案。当提出初始化空委托的概念时,我会感到不安。 - Greg
2
哇,第一次看到= delegate {}时,我觉得它很方便。不过这个+1真是太棒了,回想起来显然易见,该死 :) - anton.burger
handler?.Invoke(sender, e) - Michael Trullas Garcia

11
在C# 6.0中,由于条件空值运算符?.的出现,无需采取任何措施来进行空检查。 文档解释了调用MyEvent?.Invoke(...)会将事件复制到临时变量中,执行空检查,并在不为空的情况下在临时副本上调用Invoke。虽然这并非在每个方面都是线程安全的,因为在将事件复制到临时变量之后,有人可能已经添加了新事件,但不会被调用。它确保您不会对null调用Invoke
简而言之:
public delegate void MyClickHandler(object sender, string myValue);
public event MyClickHandler Click;

public void DoSomething() {
    Click?.Invoke(this, "foo");
}

3
我还要补充一点,就是在委托为空时,MyEvent?.Invoke(...)MyEvent(...) 的性能差异很大,根据我的测试结果:使用 .? 的方式比空委托方法快约40%。你可以查看这个链接 https://gist.github.com/didii/c4e8ef021fb8b9fca7898d71eb0de79a 来查看我用来测试的代码。 - Didii

6

您不需要为不同的事件处理程序编写多个扩展方法,只需要一个:

public static class EventHandlerExtensions {
  public static void Raise<T>(this EventHandler<T> handler, object sender, T args) where T : EventArgs {
    if (handler != null) handler(sender, args);
  }
}

6
您可以这样写:

您可以这样写:

MyEvent += delegate { };

我不确定你想做的是否正确。


我真的相信,在开发应用程序时,将空委托添加到每个事件中是正确的方法。 但我相信,有时会出现快速且简单的解决方案来处理某些情况。 - TcKs

2

这是一个不好的想法,因为现在使用事件的代码期望带有事件的对象默认已经编码了一个动作。如果您的代码永远不会被别人使用,那么我猜你可以这样做。


我同意,正如我在leppie的回答中所评论的那样。 +1 - TcKs

1

C#事件声明不幸包含了许多众所周知的安全问题和低效率。我设计了一些委托的扩展方法,以安全地调用它们,并以线程安全的方式注册/注销委托

你旧的事件触发代码:

if (someDelegate != null) someDelegate(x, y, z);

你的新代码:

someDelegate.Raise(x, y, z);

你的旧活动注册代码:

event Action fooEvent;
...
lock (someDummyObject) fooEvent += newHandler;

你的新代码:

Action fooEvent;
...
Events.Add(ref fooEvent, newHandler);

不需要锁定,也不使用编译器插入的虚拟对象来锁定事件。


-1

你可以使用PostSharp在构建时添加这个魔法。这是最好的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接