C# 中的 null 包容运算符 “!” —— 概念、由来、用法和注意事项
在 2020 年的最后一天,博客园发起了一个开源项目:基于 .NET 的博客引擎 fluss,我抽空把源码下载下来看了下,发现在属性的定义中,有很多地方都用到了 null!,如下图所示:

这是什么用法呢?之前没有在项目中用过,所以得空就研究了一下。
以前,! 运算符用来表示 “否”,比如不等于 !=。在 C# 8.0 以后,! 运算符有了一个新意义—— null 包容运算符,用来控制类型的可空性。要了解 null 包容运算符,首先就要了解可为 null 的引用类型。
可为 null 的引用类型
C# 8.0 引入了可为 null 的引用类型,与可空类型补充值类型的方式一样,它们以相同的方式补充引用类型。也就是说,通过将 ? 追加到某引用类型,可以将变量声明为可以为 null 的引用类型。 例如,string? 表示可以为 null 的 string。使用这些新类型可以更清楚地表达代码设计的意图 —— 比如将某些变量声明为 必须始终具有值,而其他一些变量声明为 可以缺少值。
借助这个定义,我们在定义引用类型的变量或属性时,便有了两种选择:
- 假定引用不可以为
null。 当变量定义为不可以为null时,编译器会强制执行规则——确保在不检查它们是否为null的前提下,取消引用这些变量是安全的:- 变量必须初始化为非
null值。 - 变量永远不能赋值为
null。
- 变量必须初始化为非
- 假定引用可以为
null。 当变量定义为可以为null时,编译器会强制执行不同的规则——确保您自己已正确检查null引用:- 只有当编译器可以保证该值不为
null时,才可以取消引用该变量。 - 这些变量可以用默认的
null值进行初始化,也可以在其他代码中赋值为null。
- 只有当编译器可以保证该值不为
与 C# 8.0 之前对引用变量的处理相比,这个新功能提供了显著的优势。在早期版本中,不能通过变量的声明来确定设计意图,编译器没有为引用类型提供针对 null 引用异常的安全性。
通过添加可为 null 的引用类型,您可以更清楚地声明您的意图。null 值是表示一个变量不引用值的正确方法,请不要使用此功能从代码中删除所有的 null 值。而是,应向编译器和阅读代码的其他开发人员声明您的意图。通过声明意图,编译器会在您编写与该意图不一致的代码时警告您。
是不是读起来有点绕?还是直接看示例比较容易理解些,请继续往下看。首先,我们来
启用可为 null 的引用类型
有三种方法可以启用可为 null 的引用类型。
在项目文件中启用
<Nullable>enable</Nullable>
将上面这一行添加到项目文件中,为当前项目启用 可为 null 的引用类型,如下图所示:

在自定义项目属性中启用
在 Directory.Build.props 文件中可以为目录下的所有项目启用 可为 null 的引用类型, 下面截图是 fluss 项目中的设置:

使用预处理器指令启用
可以使用 #nullable enable 和 #nullable disable 预处理器指令在代码中的任意位置启用和禁用 可为 null 的引用类型:

举例说明
典型用法
假设有这个定义:
class Person
{
public string? MiddleName;
}
如下这样调用:
void LogPerson(Person person)
{
Console.WriteLine(person.MiddleName.Length); // 警告 CS8602 解引用可能出现空引用。
Console.WriteLine(person.MiddleName!.Length); // 没有警告
}

这个 ! 运算符基本上就是关闭了编译器的空检查。
内部运行机制
使用此运算符告诉编译器可以安全地访问可能为 null 的内容。您可以用它来表达在这种情况下“不关心” null 安全性。
当我们讨论到 null 安全性时,一个变量可以有两种状态:
- Nullable : 可以为
null。 - Non-Nullable :不可以为
null。
从 C# 8.0 开始,所有的引用类型默认都是 Non-nullable。
“可空性”可以通过以下两个新的类型运算符进行修改:
!:从 Nullable 改为 Non-Nullable?:从 Non-Nullable 改为 Nullable
这两个运算符是相互对应的。您使用这两个运算符限定变量,然后编译器根据您的限定来确保 null 安全性。
? 运算符的用法
- Nullable:
string? x;x是引用类型,因此默认是不可以为null的。- 我们使用
?运算符将其改为可以为null的。 x = null;赋值正常,没有警告。
- Non-Nullable:
string y;y是引用类型,因此默认是不可以为null的。y = null;赋值会产生一个警告,因为您给一个声明为不支持null的变量分配了一个null值。
如下图:

! 运算符的用法
string x;
string? y = null;
x = y;- 非法!警告:将 null 文本或可能的 null 值转换为不可为 null 类型(
y可能为null)。 - 赋值运算符
=左边是不可以为null的,但右边是可以为null的。
- 非法!警告:将 null 文本或可能的 null 值转换为不可为 null 类型(
x = y!;- 合法!
- 赋值运算符
=左右两边都是不可以为null的。 - 因为
y!使用了!运算符到y,使得右边也变成了不可以为null的,所以赋值没有问题。
如下图:

️ 警告:
null包容运算符!仅在类型系统级别关闭编译器检查;在运行时,该值仍然可能是null。
这是反模式的
C# 编程时应该尽量避免使用 null 包容运算符 !。
有一些有效的使用场景(在下面会介绍),比如单元测试,使用这个运算符是适合的。不过,在 99% 的情况下,使用替代解决方案会更好。请不要只是为了取消警告,而在代码中打几十个 !。要想清楚您的场景是否真的值得使用它。
可以使用,但要小心使用。如果没有实际的目的或使用场景,请不要使用它。
null 包容运算符 ! 抵消了您获得的编译器保证的 null 安全性的作用!
使用 ! 运算符将导致很难发现 bug。如果您定义了一个标记为不可以为 null 的属性,您也就假定了可以安全地使用它。但是在运行时,您却突然遇到 NullReferenceException 异常而挠头,因为一个值在用 ! 绕过了编译器检查后,实际上却变成了 null,这不是给自己添麻烦吗?
既然这样,那么,
为什么 ! 运算符会存在?
- 在某些边缘情况下,编译器无法检测到可以为 null 的值实际上是不为
null的。 - 使遗留代码库迁移更容易。
- 在某些情况下,您根本不关心某些内容是否为
null。 - 在进行单元测试时,您可能想要检查传递
null时的代码行为。
接下来,我们继续看下:
null! 是什么意思呢?
null! 是在告诉编译器 null 不是 null 值,这听起来很怪异,是不是?
实际上,它和上面例子中的 y! 一样。它只是看起来挺怪异,因为您将该运算符用在了 null 字面量上,但概念是一样的。
我们再来看一下文章开头提到的 fluss 源码中的一行代码:
/// <summary>
/// 所属的博客。
/// </summary>
public BlogSite BlogSite { get; set; } = null!;
这行代码定义了一个名称为 BlogSite、类型为 BlogSite 的不可以为 null 的类属性。因为它是不可以为 null 的,因此单从技术上讲,很明显它是不可以被赋值为 null的。
但是,您可以通过使用 ! 运算符,将 BlogSite 属性赋值为 null。因为,就编译器所关心的 null 安全性而言,null! 不是 null。
总结
看到这里,想必您肯定已经明白了 null! 是什么意思,也学会了 null 包容运算符 ! 的概念、由来和用法。但是正如我在文中提到的那样,编程时应该尽量避免使用 !,因为它抵消了您本可以获得的编译器保证的 null 安全性;而且,这种写法阅读起来有点让人费解。
参考:
- https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
- https://stackoverflow.com/questions/54724304/what-does-null-statement-mean
- https://www.cnblogs.com/cmt/p/14217355.html
- https://www.meziantou.net/csharp-8-nullable-reference-types.htm
作者 : 技术译民
出品 : 技术译站
C# 中的 null 包容运算符 “!” —— 概念、由来、用法和注意事项的更多相关文章
- PHP中的null合并运算符
project: blog target: null-coalesce-operator-in-php.md date: 2015-12-30 status: publish tags: - Null ...
- null的坑 和 比较运算符、相等运算符的隐式转换问题 (在javascript中,null>=0 为真,null<=0 为真,null==0却为假,null到底是什么?)
null在关系运算中的坑 & 关系运算符的隐式转换问题 注意: 比较运算符 和 相等运算符 的 ECMAscript 语法实现不同. 比较运算符 和 相等运算符 对数据进行了隐式转换, 相当于 ...
- 关于Java中的Null
什么是Java中的Null? null在Java中是一个非常重要的概念,它最初是为了表示缺少某些东西,例如缺少用户.资源或任何东西而发明出来的.但是这也为Java程序员带来了很多麻烦,比如最常见的空指 ...
- 关于 Java 中的 Null
什么是Java中的Null? null在Java中是一个非常重要的概念,它最初是为了表示缺少某些东西,例如缺少用户.资源或任何东西而发明出来的.但是这也为Java程序员带来了很多麻烦,比如最常见的空指 ...
- 子查询中的NULL问题
子查询返回有单行,多行和null值:适用于单行子查询的比较运算符是=,>,>=,<,<=<>和!=.适用于多行子查询的比较运算符是in,not in,any和any ...
- Java中有关Null的9件事
对于Java程序员来说,null是令人头痛的东西.时常会受到空指针异常 (NPE)的骚扰.连Java的发明者都承认这是他的一项巨大失误.Java为什么要保留null呢?null出现有一段时间了,并且我 ...
- php中0," ",null和false的区别
php中很多还不懂php中0,"",null和false之间的区别,这些区别有时会影响到数据判断的正确性和安全性,给程序的测试运行造成很多麻烦.先看一个例子: <? $str ...
- 数据库中is null(is not null)与=null(!=null)的区别
在标准SQL语言(ANIS SQL)SQL-92规定的Null值的比较取值结果都为False,既Null=Null取值也是False.NULL在这里是一种未知值,千变万化的变量,不是“空”这一个定值! ...
- 转!!Java中关于Null的9个解释(Java Null详解)
对于Java程序员来说,null是令人头痛的东西.时常会受到空指针异常(NPE)的骚扰.连Java的发明者都承认这是他的一项巨大失误.Java为什么要保留null呢?null出现有一段时间了,并且我认 ...
随机推荐
- 5、Spring Cloud Ribbon
1.Ribbon简介 (1).Ribbon介绍 Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具. Ribbon是Netflix发布的开源项目,主 ...
- Day7 Scrum 冲刺博客
一.站立式会议1. 会议照片 2. 工作进度+燃尽图 团队成员 昨日完成工作 今日工作计划 遇到的困难 周梓波 添加键盘监听事件 由于游戏中计分规则不合理,进行重新设计 无 纪昂学 添加方块 ...
- 国际关注,Panda 交易所获悉美银监机构批准特许银行托管加密资产
近期,Panda 交易所注意到,根据此前与Cointelegraph分享的一份声明,美国货币监理署(OCC)正在授予联邦特许银行托管加密货币的权限. 鉴于加密钱包与其他种类资产的托管要求不同,这一问题 ...
- ACM训练赛:第20次
这次的题思维都很强,等之后的考试结束会集中精力重新训练一些思维题. A - A simple question CodeForces - 520B 思路: 直接看的话,很容易发现如果 \(n > ...
- Day10 python高级特性-- 生成器 Generator
列表生成式可以创建列表,但是受内存限制,列表容量时有限的,创建一个巨量元素的列表,不仅占用很大的存储空间,当仅仅访问前几个元素时,后面的绝大多数元素占用的空间都被浪费了. 如果list的元素可以按照算 ...
- python 全局变量与局部变量 垃圾回收机制
掌握L.E.G.B(作用域) 掌握局部作用域修改全局变量 步骤- 1.命名空间和作用域 命名空间:变量名称与值的映射关系作用域:变量作用的区域,即范围. 注意:class/def/模块会产生作用域:分 ...
- [日常摸鱼]bzoj1470[noi2002]Savage
晚上做到的一个扩欧的水题(?) wa了好几发感觉自己药丸-重新推了一遍公式才发现自己打错了orz 借此复习一下扩欧吧-orz 题目:http://www.lydsy.com/JudgeOnline/p ...
- Spring Boot 简单入门案例
第一:打开idea 找到spring Initializr 第二:点击Next 在点击下一步 找到web之后勾选第一个spring web 就完成了 在写一个类 点击运行 结果如下:
- SQL精华总结索引类型优化SQL优化事务大表优化思维导图❤️
索引类型 从数据结构角度: B+树索引, hash索引,基于哈希表实现,只有全值匹配才有效.以链表的形式解决冲突.查找速度非常快 O(1) 全文索引,查找的是文本中的关键词,而不是直接比较索引中的值, ...
- 常用的一句话反弹shell总结
文章转载来源:https://blog.csdn.net/qq_38684504/article/details/90047213#1.%20bash%E7%9B%B4%E6%8E%A5%E5%8F% ...