使用null条件运算符调用事件处理程序
对于刚接触事件处理的开发人员来说,会觉得触发事件是一个非常容易的事情,只需要把事件定义好在触发的时候调用相关事件就可以了。但是实际上触发事件不是那么的简单,我们在这里考虑两个问题:
- 如果在程序中根本没有任何一个处理程序和某个事件关联,会出现什么情况?
- 如果存在多个线程都要检测并调用同一个事件,这些线程之间又存在争夺的问题,会出现什么情况? 针对上面这两个问题,在 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 方法触发事件和以前的写法是完全相同的。
- Selenium2+python自动化52-unittest执行顺序
- 基于TensorFlow实现自编码器(附源码)
- Selenium2+python自动化53-unittest批量执行(discover)
- HTML/CSS/JavaScript学习笔记【持续更新】
- Selenium2+python自动化54-unittest生成测试报告(HTMLTestRunner)
- Selenium2+python自动化55-unittest之装饰器(@classmethod)
- 每天一个Linux命令(4)——mkdir
- 每天一个Linux命令(3)——pwd
- 11-移动端开发教程-zepto.js入门教程
- 【OpenCV学习笔记之一】图像加载,修改及保存
- 【干货】一种直观的方法认识梯度下降
- 漫谈Java IO之普通IO流与BIO服务器
- 浅谈强化学习的方法及学习路线
- 【亲测有效】Win10家庭版Microsoft Edge页面出现乱码的两种解决方案及gpedit.msc命令无法使用的解决策略
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Linux执行可执行文件提示No such file or directory的解决方法
- 详解bash中的脚本调试机制
- 在 Linux 上查看和配置密码时效的方法
- CentOS7中使用shell脚本安装python3.8环境(推荐)
- linux定时任务的一些相关操作汇总
- Linux nohup命令原理及实例解析
- 基于centos7快速安装mysql5.7教程解析
- Centos8下django项目部署 nginx+uwsgi的教程
- 3分钟短文:Laravel把数据验证的手伸向“请求体”
- 「Redis」字符串
- Elasticsearch:pipeline aggregation 介绍
- Qt音视频开发32-Onvif网络设置
- 3分钟短文:说说Laravel模型关联关系最单纯的“一对一”
- Redis 缓存性能实践及总结
- 如何优雅的在react-hook中进行网络请求