我经常听到/读到以下建议:
在检查事件是否null
并触发它之前,始终要对事件进行复制。这将消除线程中事件在检查null和触发事件之间的位置变成null
的潜在问题:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
更新:我认为从阅读优化,这可能也需要事件成员是volatile,但Jon Skeet在他的回答中说,CLR不会优化掉副本。
但与此同时,为了让这个问题发生,另一个线程必须做这样的事情:
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
实际的序列可能是这样的:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
问题是OnTheEvent
在作者取消订阅后运行,但他们只是取消订阅以避免这种情况发生。当然,真正需要的是在add
和remove
访问器中具有适当同步的自定义事件实现。此外,如果在触发事件时持有锁,则可能存在死锁的问题。
那么这个是《货物崇拜》节目吗?看起来是这样的——许多人必须采取这一步来保护他们的代码不受多线程的影响,而实际上在我看来,事件在被用作多线程设计的一部分之前需要更多的注意。因此,那些没有额外注意的人也可以忽略这个建议——这对于单线程程序来说根本不是问题,事实上,考虑到大多数在线示例代码中没有volatile
,这个建议可能根本没有作用。
(在成员声明中分配空的delegate { }
,这样你就不需要首先检查null
,这不是更简单吗?)
更新:以防不清楚,我确实掌握了建议的意图——在所有情况下避免空引用异常。我的观点是,这个特殊的空引用异常只会发生在另一个线程从事件中删除的情况下,这样做的唯一原因是确保不会通过该事件接收到进一步的调用,这显然不是通过这种技术实现的。您将隐藏一个竞争条件-最好是揭示它!该空异常有助于检测组件的滥用。如果你想保护你的组件不被滥用,你可以遵循WPF的例子——在你的构造函数中存储线程ID,然后在另一个线程试图直接与你的组件交互时抛出一个异常。或者实现一个真正的线程安全组件(这不是一个简单的任务)。
所以我认为,仅仅做这种复制/检查的习惯是盲目的编程,会给你的代码增加混乱和噪音。要真正防范其他线程,需要做更多的工作。
针对Eric Lippert博客文章的更新:
因此,关于事件处理程序,我忽略了一个重要的事情:“事件处理程序需要健壮地面对被调用,即使在事件已取消订阅”,显然,因此我们只需要关心事件委托为null
的可能性。对事件处理程序的需求在任何地方都有记录吗?
因此:“有其他方法来解决这个问题;例如,初始化处理程序以拥有一个永远不会删除的空操作。但做空检查是标准模式。”
另一种方法,分配空委托,只需要将= delegate {}
添加到事件声明中,这就消除了每个引发事件的地方的那些臭仪式。可以很容易地确保实例化空委托的成本很低。还是我还遗漏了什么?
当然,就像Jon Skeet所建议的那样,这只是。net 1。X条没有消失的建议,就像它在2005年应该消失的那样?
更新
从c# 6开始,这个问题的这个问题的答案是:
SomeEvent?.Invoke(this, e);