对于刚接触事件处理的开发人员来说,会觉得触发事件是一个非常容易的事情,只需要把事件定义好在触发的时候调用相关事件就可以了。但是实际上触发事件不是那么的简单,我们在这里考虑两个问题:
* 如果在程序中根本没有任何一个处理程序和某个事件关联,会出现什么情况?
* 如果存在多个线程都要检测并调用同一个事件,这些线程之间又存在争夺的问题,会出现什么情况?
针对上面这两个问题,在 C# 6.0 中新增的 null 条件运算符就可以解决这个问题。下面我们先来看一下简单的代码段。 //不安全的方式 public
class EnventSource { private int count; private EventHandler<int> Updated;
public void RaiseUpdates() { count++; Updated(this,count); } }
上面的代码中存在一个问题,如果对象触发 Updated事件时并没有相关的事件处理程序和它关联,这时就会出现 NullReferenceException
问题,在 C#6.0 出来之前如果要解决这个问题我们需要在每次触发前都要去判断以下事件处理程序是否为 null:
//C#6.0以前的处理方式 public class EnventSource { private int count; private
EventHandler<int> Updated; public void RaiseUpdates() { count++; if(Updated!=
null){ Updated(this,count); } } }
经过修正后的代码可以在绝大部分情况下解决前面所提到的问题。注意我这里说的时绝大部分情况,还有一种特殊的情况会出现前面所提的问题,比如 A 线程在执行完 if
语句后发现 Updated 并不等于空,这时在 A 线程还没开始执行 Updated(this,count) 语句时 B
线程将事件处理程序的订阅解除了,那么在 A 线程执行到 Updated(this,count) 语句时事件处理程序已经为 null 了,这样仍然会出现
NullReferenceException 问题。针对这个问题开发人员会进行如下的处理:
//C#6.0以前的处理方式进一步修改 public class EnventSource { private int count; private
EventHandler<int> Updated; public void RaiseUpdates() { count++; var handler=
Updated; if(handler!=null){ handler(this,count); } } }
上面的代码完美的处理的前面所说的问题,但是这样的代码会造成不易理解,我为什么修改成这样就是线程安全的呢?这是因为我们把事件处理程序赋值给了一个新的局部变量,这个局部变量就包含了多播委托,这个委托就可以应用原来的那个委托的所有成员变量里的事件处理程序。这种方法叫做浅拷贝,也就是创建了一个新的引用并让它指向了原来的事件处理程序。当一个线程把事件处理程序注销掉时,它只是修改的类实例中
Updated 子字段,而不是把处理程序从 handler 中移除掉。简单地说 handler 其实时 Updated
的快照,在触发事件的时候它所通知的那些事件处理程序其实是在做快照时记录下来的。这种方法虽然写法没错,但是对于新手来说是很难理解的,并且只要是在有触发事件的地方都要重复编写一边这样的代码。在
C#6.0 以后我们就可以使用 null 条件运算符来简单的处理这个问题,下面我们来看一下在 C#6.0 中如何解决这个问题。
public class EnventSource { private int count; private EventHandler<int>
Updated; public void RaiseUpdates() { count++; Updated?.Invoke(this.count); } }
这段代码采用了 null 条件运算符安全的调用了事件处理程序,它首先会判断 ? 号左侧内容是否为 null,如果不为 null
则执行右侧的内容,反之跳过该语句执行下一条语句。这种方式的优势在于和以前使用 if 的方式相比,运算符左侧的内容只会计算一次。但是这里又有需要注意的地方,因为
C# 不允许在 ?. 后面出现括号,因此我们必须使用 Invoke 方法去触发事件,每定义一个委托或者事件编译器就会生成类型安全的 Invoke
方案,这就表明通过调用 Invoke 方法触发事件和以前的写法是完全相同的。