为什么C#要求您在每次触发事件时都写一个空检查?

这对我来说似乎很奇怪 – VB.NET通过其RaiseEvent关键字隐式处理空检查。 它似乎大大增加了围绕事件的样板数量,我看不出它提供了什么好处。

我确信语言设计师有充分的理由这样做..但我很好奇,如果有人知道为什么。

这当然是一个烦恼的问题。

当您编写访问类中类字段事件的代码时,您实际上是在访问字段本身(在C#4中进行一些更改模式;暂时不要去那里)。

所以,选项将是:

  • 特殊情况类字段事件调用,以便它们实际上不直接引用该字段,而是添加了一个包装器
  • 以不同方式处理所有委托调用,例如:

     Action x = null; x(); 

    不会抛出exception。

当然,对于非void委托(和事件),这两个选项都会引发一个问题:

 Func x = null; int y = x(); 

应该默默地返回0吗? ( int的默认值。)或者它实际上是掩盖了一个bug(更有可能)。 让它静静地忽略你试图调用空委托的事实会有些不一致。 在这种情况下甚至更奇怪,它不使用C#的语法糖:

 Func x = null; int y = x.Invoke(); 

基本上,无论你做什么,事情都变得棘手并且与语言的其他部分不一致。 我也不喜欢它,但我不确定什么是实用但一致的解决方案……

我们通常通过声明我们的事件来解决这个问题:

 public event EventHandler Foo = delegate { }; 

这有两个好处。 首先是我们没有检查null。 第二个是我们避免在典型的事件触发中无处不在的关键部分问题:

 // old, busted code that happens to work most of the time since // a lot of code never unsubscribes from events protected virtual void OnFoo(FooEventArgs e) { // two threads, first checks for null and succeeds and enters if (Foo != null) { // second thread removes event handler now, leaving Foo = null // first thread resumes and crashes. Foo(this, e); } } // proper thread-safe code protected virtual void OnFoo(FooEventArgs e) { EventHandler handler = Foo; if (handler != null) handler(this, e); } 

但是,随着Foo自动初始化为空委托,永远不需要任何检查,代码自动是线程安全的,并且更容易读取引导:

 protected virtual void OnFoo(FooEventArgs e) { Foo(this, e); // always good } 

在“空手道小子”中向帕特·森田道歉,“避免空虚的最佳方法就是没有。”

至于为什么,C#并没有像VB那样溺爱你。 尽管event关键字隐藏了多播委托的大多数实现细节,但它确实提供了比VB更精细的控制。

如果设置管道以首先引发事件将是昂贵的(如SystemEvents)或准备事件参数将是昂贵的(如Paint事件),您需要考虑需要什么代码。

事件处理的Visual Basic风格不允许您推迟支持此类事件的成本。 您无法覆盖添加/删除访问器以延迟将昂贵的管道放置到位。 并且您无法发现可能没有订阅任何事件处理程序,因此刻录循环以准备事件参数是浪费时间。

在C#中不是问题。 方便和控制之间的经典权衡。

扩展方法提供了一种非常酷的方式来解决这个问题。 请考虑以下代码:

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

现在你可以简单地这样做:

 class Test { public event EventHandler SomeEvent; public void DoSomething() { SomeEvent.Raise(this); } } 

但是正如其他人已经提到的那样,您应该了解multithreading场景中可能存在的竞争条件。

不知道为什么会这样做,但是有一个专门针对代表的Null对象模式的变体:

 private event EventHandler Foo = (sender, args) => {}; 

这样你就可以在不检查null情况下自由调用Foo

因为RaiseEvent带来了一些开销。

在控制和易用性之间总是存在权衡。

  • VB.Net:易用性,
  • C#:更多控制为VB
  • C ++:更多的控制,更少的指导,更容易射击自己的脚

编辑:正如OP指出的那样,这个答案并没有解决问题的主体。 但是,有些人可能会发现它很有用,因为它确实为问题的标题提供了答案(当它自己采取时):

为什么C#要求您在每次触发事件时都写一个空检查?

它还为问题正文的意图提供了背景,有些人可能认为这有用。 所以,由于这些原因和Meta的这个建议 ,我会让这个答案成立。


原文:

在其MSDN文章如何:发布符合.NET Framework准则的事件(C#编程指南) (Visual Studio 2013)中 ,Microsoft在其示例中包含以下注释:

 // Make a temporary copy of the event to avoid possibility of // a race condition if the last subscriber unsubscribes // immediately after the null check and before the event is raised. 

以下是Microsoft示例代码的较大摘录,该代码为该注释提供了上下文。

 // Wrap event invocations inside a protected virtual method // to allow derived classes to override the event invocation behavior protected virtual void OnRaiseCustomEvent(CustomEventArgs e) { // Make a temporary copy of the event to avoid possibility of // a race condition if the last subscriber unsubscribes // immediately after the null check and before the event is raised. EventHandler handler = RaiseCustomEvent; // Event will be null if there are no subscribers if (handler != null) { // Format the string to send inside the CustomEventArgs parameter e.Message += String.Format(" at {0}", DateTime.Now.ToString()); // Use the () operator to raise the event. handler(this, e); } } 

请注意,从C#6开始,该语言现在提供了一种简洁的语法来方便地执行此空检查。 例如:

 public event EventHandler SomeEvent; private void M() { // raise the event: SomeEvent?.Invoke(this, EventArgs.Empty); } 

请参阅Null条件运算符

原因真的归结为C#给你更多的控制权。 在C#中,如果您愿意,则不必进行null检查。 在下面的代码中, MyEvent永远不会为null那么为什么还要费心去做检查呢?

 public class EventTest { private event EventHandler MyEvent = delegate { Console.WriteLine("Hello World"); } public void Test() { MyEvent(this, new EventArgs()); } }