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的内容。您可以用它来表达在这种情况下“不关心”
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
的。 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安全性;而且,这种写法阅读起来有点让人费解。
有朋友说文章内容不太容易看懂,我补充两张图帮助理解一下:
C# 8.0之前:
C# 8.0之后:
参考:
- 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
作者 : 技术译民
出品 : 技术译站
- SQL开发中容易忽视的一些小地方(一)【null的用法及注意事项】
- 抽象类概念用法及注意事项
- innerText,innerHTML的用法以及注意事项
- Android数据存储方式——SQLite数据库的增删改查用法样例和注意事项.txt
- 自增运算符“++”的使用注意事项
- vector中erase用法注意事项
- C# windows服务 的注意事项
- c++ assert 的用法和注意事项
- java中使用switch-case的用法及注意事项超全总结
- C#调用C库的注意事项
- 多线程CreateThread函数的用法及注意事项
- c#事务的使用、示例及注意事项
- C#语言中变量的使用和注意事项
- Jdbctemplate的update方法地简单底层实现与BeanPropertyRowMapper的用法和注意事项
- bootstrap-datetimepicker 自定义用法及注意事项
- typedef的一些用法及注意事项
- js中defer用法注意事项
- c语言结构体注意事项及用法
- JavaScript: switch用法及注意事项
- C#【2】串口编程:如果要自测,串接哪几个口?注意事项有些什么、并举例?