不管是刚接触 C# 还是已经具有多年开发经验的大部分人会觉得事件处理很简单,只需要把事件定义好然后在需要的时候出发它就可以了。其实这种想法是错误的,这里面有很多需要注意的问题。下面这段代码是大部分开发人员经常使用的定义事件处理程序的方法。
public class EventDemo { private EventHandler<int> demo; public void DemoEvent() { demo(this); } }
上面的代码中存在一个严重的问题,当在对象上触发 demo 事件时并没有关联的事件处理程序的话,C# 将会用 null 值来表示没有处理程序与该事件相关联,进而将会引发 NullReferenceException 异常。针对这个问题大部分程序员会做如下修改:
public void DemoEvent() { if(demo!=null) { demo(this); } }
这种修改方法解决了上述大部分问题,但是还存在一个隐藏的问题。当有多个线程都调用这个事件是就会出现线程之间相互争夺,举个例子来说就是线程 A 在执行到 if (demo!=null)
时发现 demo 不等于 null ,正巧这时线程 B 将唯一的事件处理程序解除了订阅,这时线程 A 再调用 demo 时事件处理程序已经变为了 null ,进而导致 NullReferenceException 异常。真多这个问题一些程序员又会做如下的修改:
public void DemoEvent() { var handler = demo; if(handler!=null) { handler(this); } }
上述这种方法是对等号右侧的内容进行了浅拷贝创建了新的引用,使其指向原来的事件处理程序(相当于给事件订阅者生成了一个快照),当另一个进程注销掉事件处理程序时,注销的只是 demo 上所绑定的处理程序,因此当当前的线程执行 handler 时是不会出现 NullReferenceException 异常。这种解决方法是网上所能搜的方法之一,也是绝大部分开发人员所推荐的解决方法。但是这个方法会使代码显得难以理解(尤其是对于开发新手),并且代码稍显冗余。于是在 C# 6.0 中微软为我们增加了 null 条件运算符(?.)。null 条件运算符可以安全的调用事件处理程序并且使代码清晰明了还简单。首先它会判断运算符左侧的内容是否为 null ,如果是 null 就跳过该语句,反之执行运算符右侧的内容。下面我们利用 null 条件运算符对前面的代码进行一下改进。
public void DemoEvent() { demo?.Invoke(this); }
Tip:使用 null 条件运算符有一点需要注意,运算符右侧不允许直接出现括号,因此必须使用 Invoke 进行触发事件。每定义一个委托或者时间编译器就会生成一个 Invoke 方法。
进行触发事件。每定义一个委托或者时间编译器就会生成一个 Invoke 方法。