查看原文
其他

C# 中的 null 包容运算符"!" :用法和注意事项

DotNet 2021-09-23

The following article is from 技术译站 Author 技术译民

(给DotNet加星标,提升.Net技能

转自:技术译民
cnblogs.com/ittranslator/p/14260348.html

前言


在2020年的最后一天,博客园发起了一个开源项目:基于 .NET 的博客引擎 fluss,我抽空把源码下载下来看了下,发现在属性的定义中,有很多地方都用到了 null!,如下图所示:



这是什么用法呢?之前没有在项目中用过,所以得空就研究了一下。


以前! 运算符用来表示 “否”,比如不等于 !=。在 C# 8.0 以后,! 运算符有了一个新意义—null 包容运算符,用来控制类型的可空性。要了解 null 包容运算符,首先就要了解可为 null 的引用类型。


一、可为 null 的引用类型


C# 8.0 引入了可为 null 的引用类型,与可空类型补充值类型的方式一样,它们以相同的方式补充引用类型。也就是说,通过将 ? 追加到某引用类型,可以将变量声明为可以为 null 的引用类型。例如,string? 表示可以为 null 的 string。使用这些新类型可以更清楚地表达代码设计的意图 — 比如将某些变量声明为 必须始终具有值,而其他一些变量声明为 可以缺少值。


借助这个定义,我们在定义引用类型的变量或属性时,便有了两种选择:


1、假定引用不可以为 null。当变量定义为不可以为 null 时,编译器会强制执行规则,确保在不检查它们是否为 null 的前提下,取消引用这些变量是安全的:


  • 变量必须初始化为非 null 值


  • 变量永远不能赋值为 null


2、假定引用可以为 null。 当变量定义为可以为 null 时,编译器会强制执行不同的规则确保您自己已正确检查 null 引用:


  • 只有当编译器可以保证该值不为 null 时,才可以取消引用该变量


  • 这些变量可以用默认的 null 值进行初始化,也可以在其他代码中赋值为 null


与 C# 8.0 之前对引用变量的处理相比,这个新功能提供了显著的优势。在早期版本中,不能通过变量的声明来确定设计意图,编译器没有为引用类型提供针对 null 引用异常的安全性。


通过添加可为 null 的引用类型,您可以更清楚地声明您的意图。null 值是表示一个变量不引用值的正确方法,请不要使用此功能从代码中删除所有的 null 值。而是,应向编译器和阅读代码的其他开发人员声明您的意图。通过声明意图,编译器会在您编写与该意图不一致的代码时警告您。


是不是读起来有点绕?还是直接看示例比较容易理解些,请继续往下看。首先,我们来


二、启用可为 null 的引用类型


有三种方法可以启用可为 null 的引用类型。


1、在项目文件中启用


<Nullable>enable</Nullable>


将上面这一行添加到项目文件中,为当前项目启用 可为 null 的引用类型,如下图所示:



2、在自定义项目属性中启用


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



3、使用预处理器指令启用


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



三、举例说明


1、典型用法


假设有这个定义:


class Person
{
public string? MiddleName;
}



如下这样调用:


void LogPerson(Person person)
{
Console.WriteLine(person.MiddleName.Length); // 警告 CS8602 解引用可能出现空引用。
Console.WriteLine(person.MiddleName!.Length); // 没有警告
}




这个 ! 运算符基本上就是关闭了编译器的空检查。


2、内部运行机制


使用此运算符告诉编译器可以安全地访问可能为 null 的内容。您可以用它来表达在这种情况下“不关心” null 安全性。


当我们讨论到 null 安全性时,一个变量可以有两种状态:


  • Nullable : 可以为 null


  • Non-Nullable :不可以为 null


从 C# 8.0 开始,所有的引用类型默认都是 Non-nullable。


“可空性”可以通过以下两个新的类型运算符进行修改:


  • ! :从 Nullable 改为 Non-Nullable


  • ? :从 Non-Nullable 改为 Nullable


这两个运算符是相互对应的。您使用这两个运算符限定变量,然后编译器根据您的限定来确保 null 安全性。


3、? 运算符的用法


Nullable:string? x;


  • x 是引用类型,因此默认是不可以为 null


  • 我们使用 ? 运算符将其改为可以为 null 


  • x = null; 赋值正常,没有警告


Non-Nullable:string y;


  • y 是引用类型,因此默认是不可以为 null 的


  • y = null; 赋值会产生一个警告,因为您给一个声明为不支持 null 的变量分配了一个 null 值。


如下图:



4、! 运算符的用法


string x;
string? y = null;


x = y;


  • 非法!警告:将 null 文本或可能的 null 值转换为不可为 null 类型(y 可能为 null)


  • 赋值运算符 = 左边是不可以为 null 的,但右边是可以为 null 的


x = y!;


  • 合法!


  • 赋值运算符 = 左右两边都是不可以为 null 的。


  • 因为 y! 使用了 ! 运算符到 y,使得右边也变成了不可以为 null 的,所以赋值没有问题。


如下图:



⚠️ 警告:null 包容运算符 ! 仅在类型系统级别关闭编译器检查;在运行时,该值仍然可能是 null。


四、这是反模式的


1、C# 编程时应该尽量避免使用 null 包容运算符 !


有一些有效的使用场景(在下面会介绍),比如单元测试,使用这个运算符是适合的。不过,在 99% 的情况下,使用替代解决方案会更好。请不要只是为了取消警告,而在代码中打几十个 !。要想清楚您的场景是否真的值得使用它。


💡 可以使用,但要小心使用。如果没有实际的目的或使用场景,请不要使用它。


null 包容运算符 ! 抵消了您获得的编译器保证的 null 安全性的作用!


使用 ! 运算符将导致很难发现 bug。如果您定义了一个标记为不可以为 null 的属性,您也就假定了可以安全地使用它。但是在运行时,您却突然遇到 NullReferenceException 异常而挠头,因为一个值在用 ! 绕过了编译器检查后,实际上却变成了 null,这不是给自己添麻烦吗?


既然这样,那么


2、为什么 ! 运算符会存在?


  • 在某些边缘情况下,编译器无法检测到可以为 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 安全性;而且,这种写法阅读起来有点让人费解。


- EOF -


推荐阅读  点击标题可跳转

.NET Core 处理 WebAPI JSON 返回烦人的null为空

C# + Selenium + ChromeDriver 爬取网页

性能超四倍的高性能.NET二进制序列化库


看完本文有收获?请转发分享给更多人

推荐关注「DotNet」,提升.Net技能 

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存