Effective-CSharp-8用null条件运算符调用事件处理程序
刚接触事件的人可能觉得触发事件是很容易的,只需将事件定义好,并在需要触发时调用相关的事件处理程序就可以了,底层的多播委托将会依次执行这些处理程序。实际上触发事件并不是如此简单。若根本没有事件对应的处理程序会怎样?若多个线程都要检测并调用事件处理程序,而线程之间互相争夺,会怎样?C# 6.0 引入的 null 条件运算符 (null-conditional operator,又称 null 传播运算符 (null-propagation operator)) 可用更加清晰的写法来解决这些问题。
1 | public class EventSource |
这种旧写法有个明显的问题:如在对象上出发 Updated 事件是并没有事件处理程序与之相关,将会发生 NullReferenceException,因为 C# 用 null 值表示这种没有处理程序与之相关的情况。于是,触发事件前,必须先判断事件处理程序是否为 null。
1 | public void RaiseUpdates() |
但这种写法依然有 bug。当程序线程执行完 if 判断了 Updated 不为 null 后,可能会有另一个线程打断该线程,并解除订阅,这样的话依然会引发 NullReferenceException,虽然这种情况很少见。
这个 bug 很难诊断,也很难修复。想重现该错误,必须按照上述线程的执行顺序执行。一些开发老手在此问题上吃过亏,他们知道其危险,改用另一个种写法:
1 | public void RaiseUpdates() |
此方法是可行的,线程是安全的。但是从阅读的角度来看,看代码的人不太明白为何这样改后就能确保线程安全。
var handler = Updated; 这是对赋值号的右侧做 *浅拷贝 (shallow copy)*,也就是创建一个新的引用,指向其事件处理程序。因此,即使是 Updated 被其他线程注销,变为 null。也不会影响 handler,handler 依然保存了原先记录的事件订阅者。这段代码实际上是通过浅拷贝为事件订阅这做了份快照,触发事件时通过快照来触发事件处理程序。
触发事件是一项简单的任务,不该用这么冗长且费解的方式去完成。
有了 null 条件运算符,可以用更清晰的写法来实现:
1 | public void RaiseUpdates() |
采用了 null 条件运算符 (也就是 ?.) 安全地调用事件处理程序。该运算符首先对左侧内容进行 null 判断,若非 null 执行右侧内容。若为 null 则跳过此语句。
从语义上来说这和 if 类似。但区别在于 ?. 运算符左侧的内容只会计算一次。
由于 C# 不许 ?. 运算符右侧直接出现一对括号,因此必须用 Invoke() 去触发事件。每定义一种委托或事件,编译器都会为此生成类型安全的 Invoke(),这意味着,通过 Invoke 方法触发事件,使得代码篇幅更小,且线程安全。
有了这种简单且清晰的写法后,原来的写法需要改一改了。以后触发事件都应采用此写法。