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

时间:2009-05-08 16:36:01

标签: .net events delegates

目前,“Avoid checking for null event handlers"位于帖子标题为Hidden Features of 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);  
}

乍一看,这似乎是一个聪明的捷径,但它可能是大型应用程序中一些严重问题的原因,尤其是涉及并发时。

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

这样的事情很典型:

void DoSomething()
{
    if(SomeEvent != null) 
        SomeEvent(this, EventArgs.Empty);
}

但即使在上面的例子中,也存在这样的可能性,即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);
    }
}

有关.NET中EventHandlers主题的广泛讨论,我建议阅读Krzysztof Cwalina和Brad Abrams的“Framework Design Guidelines”,第5章,第4节 - 事件设计。特别是Eric Gunnerson和Joe Duffy对该主题的讨论。

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

希望主持人会对相关问题the answer进行注释,以便毫无疑问的读者不会被糟糕的模式误导。

6 个答案:

答案 0 :(得分:26)

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

C# Events and Thread Safety

你的摘要没有做任何说服我的事情!

首先,该类的客户端无法为该事件指定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...
    }
}

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

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

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

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

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

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

答案 1 :(得分:15)

“仅仅因为您使用空委托初始化事件并不意味着您的类的用户不会在某个时刻将其设置为null并破坏您的代码。”

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

答案 2 :(得分:3)

只是澄清一下。使用空委托作为事件初始值的方法即使与序列化一起使用也是如此:

// to run in linqpad:
// - add reference to System.Runtime.Serialization.dll
// - add using directives for System.IO and System.Runtime.Serialization.Formatters.Binary
void Main()
{
    var instance = new Foo();
    Foo instance2;
    instance.Bar += (s, e) => Console.WriteLine("Test");
    var formatter = new BinaryFormatter();
    using(var stream = new MemoryStream()) {
        formatter.Serialize(stream, instance);
        stream.Seek(0, SeekOrigin.Begin);
        instance2 = (Foo)formatter.Deserialize(stream);
    }
    instance2.RaiseBar();
}

[Serializable]
class Foo {
    public event EventHandler Bar = delegate { };
    public void RaiseBar() {
        Bar(this, EventArgs.Empty);
    }
}

// Define other methods and classes here

答案 3 :(得分:1)

就像一张纸条, http://blogs.msdn.com/ericlippert/archive/2009/04/29/events-and-races.aspx

这是Erik所引用文章的永久链接。

答案 4 :(得分:0)

对于它的价值,你应该真正关注Juval Lowy的EventsHelper class,而不是自己做事。

答案 5 :(得分:-1)

Brumme是埃里克和艾布拉姆斯的父亲。你应该阅读他的博客,而不是从两位公关人员中任何一位讲道。这家伙是严谨的技术(而不是高级发型师标识)。他会给你一个正确的解释,没有“1TB土地上的雷德蒙德花”,为什么种族和记忆模型对于上面另一张海报所提出的有管理(re:shield-the-children)环境的挑战。

不过,这一切都始于他们,C ++ CLR实现人员:

blogs.msdn.com/cbrumme