在.NET框架中引发事件的正确方法

Mik*_*scu 28 .net events delegates

目前" 避免检查空事件处理程序"是标题为C#隐藏功能的帖子的答案的顶部,它包含严重误导性信息.

虽然我理解Stack Overflow是一个"民主",并且答案因公众投票而上升到顶峰,我觉得很多投票给答案的人要么没有完全理解C#/ .NET或者没有花时间充分了解帖子中描述的做法的后果.

简而言之,该帖子主张使用以下构造来避免在调用事件时检查null.

public event EventHandler SomeEvent = delegate {};
// Later..
void DoSomething()
{
   // Invoke SomeEvent without having to check for null reference
    SomeEvent(this, EventArgs.Empty);  
}
Run Code Online (Sandbox Code Playgroud)

乍一看,这似乎是一个聪明的捷径,但它可能是大型应用程序中一些严重问题的原因,特别是如果涉及并发性.

在调用事件的委托之前,您必须检查空引用.仅仅因为您使用空委托初始化事件并不意味着您的类的用户不会在某些时候将其设置为null并破坏您的代码.

像这样的东西是典型的:

void DoSomething()
{
    if(SomeEvent != null) 
        SomeEvent(this, EventArgs.Empty);
}
Run Code Online (Sandbox Code Playgroud)

但即使在上面的例子中,也存在这样的可能性,即DoSomething()可能由一个线程运行,另一个可能会删除事件处理程序,并且可能会出现竞争条件.

假设这种情况:

      Thread A.                           Thread B.
    -------------------------------------------------------------------------
 0: if(SomeEvent != null)
 1: {                                     // remove all handlers of SomeEvent
 2:   SomeEvent(this, EventArgs.Empty);
 3: }

在引发事件的代码检查了委托以获取空引用之后,但在调用委托之前,线程B删除了SomeEvent事件的事件处理程序.当SomeEvent(this,EventArgs.Empty); 调用,SomeEvent为null并引发异常.

为了避免这种情况,提出事件的更好模式是:

void DoSomething()
{
    EventHandler handler = SomeEvent;
    if(handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}
Run Code Online (Sandbox Code Playgroud)

有关.NET中EventHandlers主题的广泛讨论,我建议阅读Krzysztof Cwalina和Brad Abrams的" 框架设计指南 ",第5章,第4节 - 事件设计.特别是Eric Gunnerson和Joe Duffy对该主题的讨论.

正如Eric所建议的,在下面的一个答案中,我应该指出可以设计一个更好的同步解决方案来解决问题.我在这篇文章中的目标是提高意识,不是为问题提供一个唯一真正的解决方案.正如Eric Lippert和Eric Gunnerson在上面提到的书中所建议的那样,问题的具体解决方案取决于程序员,但重要的是这个问题不能被忽视.

希望主持人能够有问题的答案进行注释,以便毫无疑问的读者不会被错误的模式误导.

Dan*_*ker 26

一周前我提出了同样的问题并得出了相反的结论:

C#事件和线程安全

你的总结没有做任何事情来说服我否则!

首先,该类的客户端无法为该事件指定null.这是event关键字的重点.没有该关键字,它将是一个持有代表的字段.有了它,除了入伍和退市之外,它上面的所有操作都是私有的.

因此,delegate {}在构造中分配事件完全满足正确实现事件源的要求.

当然,在课堂上可能存在事件设置的错误null.但是,在包含任何类型字段的任何类中,可能存在将字段设置为的错误null.您是否主张每次访问类的任何成员字段时,我们都会编写这样的代码?

// field declaration:
private string customerName;

private void Foo()
{
    string copyOfCustomerName = customerName;
    if (copyOfCustomerName != null)
    {
        // Now we can use copyOfCustomerName safely...
    }
}
Run Code Online (Sandbox Code Playgroud)

当然你不会.所有程序都会变成两倍长,一半可读,这是没有充分理由的.当人们将这种"解决方案"应用于事件时,就会发生同样的疯狂.事件不是公开的,与私有字段相同,因此直接使用它们是安全的,只要在构造时将它们初始化为空委托即可.

你无法做到这一点的一种情况是你在a中有一个事件struct,但这并不是一个不便,因为事件往往出现在可变对象上(表明状态发生了变化),struct如果允许变异,s是众所周知的技巧,因此最好是不可变的,因此事件与structs 几乎没有用.

可能存在另一个完全独立的竞争条件,正如我在我的问题中描述的那样:如果客户端(事件接收器)想要确定它们的处理程序在被除名后不会被调用,该怎么办?但正如Eric Lippert指出的那样,客户有责任解决这个问题.简而言之:不可能保证事件处理程序在被除名后不会被调用.这是代表不可改变的必然结果.无论是否涉及线程,都是如此.

在Eric Lippert的博客文章中,他链接到我的SO问题,但随后陈述了一个不同但相似的问题.我认为他这样做是为了一个合法的修辞目的 - 只是为了讨论关于次要竞争条件的讨论,影响事件处理者.但不幸的是,如果你按照我的问题的链接,然后稍微不经意地阅读他的博客文章,你可能会得到他正在驳回"空委托"技术的印象.

事实上,他说"还有其他方法可以解决这个问题;例如,初始化处理程序以便有一个永不删除的空操作",这就是"空委托"技术.

他介绍了"做空检查",因为它是"标准模式"; 我的问题是,为什么这是标准模式?Jon Skeet建议,鉴于该建议早于将匿名函数添加到语言中,它可能只是C#版本1的宿醉,我认为这几乎肯定是正确的,所以我接受了他的答案.

  • 我认为首先应该从您在注册之前看到的页面开始说"我明白过早优化是所有邪恶的根源,我承诺永远不会使用猜测来选择使我的程序运行得更快的方法,而是实现最干净的设计和代码结构,并使用分析器来识别实际需要应用凌乱优化的瓶颈." (13认同)
  • 我希望编译器可以生成空检查. (2认同)

wek*_*mpf 15

"仅仅因为你使用空委托初始化事件并不意味着你的类的用户不会在某些时候将它设置为null并破坏你的代码."

不可能发生.事件"只能出现在+ =或 - =的左侧(除非在类型中使用)"以引用您在执行此操作时将获得的错误.当然,"从类型中使用的除外"使这成为理论上的可能性,但不是任何理智的开发者都会关注的.

  • 嗯,公平地说,有一种方法我可以想到在这里得到一个空值:我相信序列化会将它设置为null.但是,这又是控制类实现的东西. (5认同)
  • 也可以使用序列化,请参阅我的回答. (2认同)

Jef*_*ser 0

就其价值而言,您确实应该研究 Juval Lowy 的EventsHelper 类,而不是自己做事。

  • 杰夫,我并不是提倡在这里重新发明轮子。我只是想说明为什么需要检查 null ,以便人们能够理解为什么这样做很重要(如果自己做事情太难的话,他们可能会寻找一个帮助类......) (2认同)