C# 中的 null 包容运算符 “!” —— 概念、由来、用法和注意事项
<p>在 2020 年的最后一天,博客园发起了一个开源项目:基于 .NET 的博客引擎 fluss,我抽空把源码下载下来看了下,发现在属性的定义中,有很多地方都用到了 <code>null!</code>,如下图所示:</p><p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010217986-574857447.png" alt="cnblog null" loading="lazy"></p>
<p>这是什么用法呢?之前没有在项目中用过,所以得空就研究了一下。</p>
<p>以前,<code>!</code> 运算符用来表示 “否”,比如不等于 <code>!=</code>。在 C# 8.0 以后,<code>!</code> 运算符有了一个新意义—— <strong><code>null</code> 包容运算符</strong>,用来控制类型的<em>可空性</em>。要了解 <code>null</code> 包容运算符,首先就要了解<em>可为 null 的引用类型</em>。</p>
<h2 id="可为-null-的引用类型">可为 null 的引用类型</h2>
<p>C# 8.0 引入了可为 null 的引用类型,与可空类型补充<em>值类型</em>的方式一样,它们以相同的方式补充<em>引用类型</em>。也就是说,通过将 <code>?</code> 追加到某引用类型,可以将变量声明为<em>可以为 null 的引用类型</em>。 例如,<code>string?</code> 表示<em>可以为 <code>null</code> 的 <code>string</code></em>。使用这些新类型可以更清楚地表达代码设计的意图 —— 比如将某些变量声明为 <strong>必须始终具有值</strong>,而其他一些变量声明为 <strong>可以缺少值</strong>。</p>
<p>借助这个定义,我们在定义引用类型的变量或属性时,便有了两种选择:</p>
<ol>
<li><strong>假定引用不可以为 <code>null</code>。</strong> 当变量定义为不可以为 <code>null</code> 时,编译器会强制执行规则——确保在不检查它们是否为 <code>null</code> 的前提下,取消引用这些变量是安全的:
<ul>
<li>变量必须初始化为非 <code>null</code> 值。</li>
<li>变量永远不能赋值为 <code>null</code>。</li>
</ul>
</li>
<li><strong>假定引用可以为 <code>null</code>。</strong> 当变量定义为可以为 <code>null</code> 时,编译器会强制执行不同的规则——确保您自己已正确检查 <code>null</code> 引用:
<ul>
<li>只有当编译器可以保证该值不为 <code>null</code> 时,才可以取消引用该变量。</li>
<li>这些变量可以用默认的 <code>null</code> 值进行初始化,也可以在其他代码中赋值为 <code>null</code>。</li>
</ul>
</li>
</ol>
<p>与 C# 8.0 之前对引用变量的处理相比,这个新功能提供了显著的优势。在早期版本中,不能通过变量的声明来确定设计意图,编译器没有为引用类型提供针对 <code>null</code> 引用异常的安全性。</p>
<p>通过添加<em>可为 <code>null</code> 的引用类型</em>,您可以更清楚地声明您的意图。<code>null</code> 值是表示一个变量不引用值的正确方法,请不要使用此功能从代码中删除所有的 <code>null</code> 值。而是,应向编译器和阅读代码的其他开发人员声明您的意图。通过声明意图,编译器会在您编写与该意图不一致的代码时警告您。</p>
<p>是不是读起来有点绕?还是直接看示例比较容易理解些,请继续往下看。首先,我们来</p>
<h2 id="启用可为-null-的引用类型">启用可为 null 的引用类型</h2>
<p>有三种方法可以启用<em>可为 null 的引用类型</em>。</p>
<h3 id="在项目文件中启用">在项目文件中启用</h3>
<pre><code class="language-xml"><Nullable>enable</Nullable>
</code></pre>
<p>将上面这一行添加到项目文件中,为当前项目启用 <em>可为 null 的引用类型</em>,如下图所示:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010311211-976904443.png" alt="nullable enable 1" loading="lazy"></p>
<h3 id="在自定义项目属性中启用">在自定义项目属性中启用</h3>
<p>在 <code>Directory.Build.props</code> 文件中可以为目录下的所有项目启用 <em>可为 null 的引用类型</em>, 下面截图是 fluss 项目中的设置:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010354652-646106571.png" alt="cnblog nullable enable 3" loading="lazy"></p>
<h3 id="使用预处理器指令启用">使用预处理器指令启用</h3>
<p>可以使用 <code>#nullable enable</code> 和 <code>#nullable disable</code> 预处理器指令在代码中的任意位置启用和禁用 <em>可为 null 的引用类型</em>:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010506565-878112687.png" alt="nullable enable 2" loading="lazy"></p>
<h2 id="举例说明">举例说明</h2>
<h3 id="典型用法">典型用法</h3>
<p>假设有这个定义:</p>
<pre><code class="language-csharp">class Person
{
public string? MiddleName;
}
</code></pre>
<p>如下这样调用:</p>
<pre><code class="language-csharp">void LogPerson(Person person)
{
Console.WriteLine(person.MiddleName.Length);// 警告CS8602解引用可能出现空引用。
Console.WriteLine(person.MiddleName!.Length); // 没有警告
}
</code></pre>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010556234-839926700.png" alt="nullable enable warning" loading="lazy"></p>
<p>这个 <code>!</code> 运算符其实就是关闭了编译器的空检查,它就是在告诉编译器或者以后维护你代码的同事:“我”肯定不是 null ,你不用做 null 安全检查了。</p>
<h3 id="内部运行机制">内部运行机制</h3>
<p>使用此运算符<em>告诉编译器可以安全地访问可能为 <code>null</code> 的内容</em>。您可以用它来表达在这种情况下“不关心” <code>null</code> 安全性。</p>
<p>当我们讨论到 <code>null</code> 安全性时,一个变量可以有两种状态:</p>
<ol>
<li>Nullable : 可以为 <code>null</code>。</li>
<li>Non-Nullable :不可以为 <code>null</code>。</li>
</ol>
<p>从 C# 8.0 开始,所有的<em>引用类型</em>默认都是 <em>Non-nullable</em>。</p>
<p>“可空性”可以通过以下两个新的<strong>类型运算符</strong>进行修改:</p>
<ol>
<li><code>!</code> :从 Nullable 改为 Non-Nullable</li>
<li><code>?</code> :从 Non-Nullable 改为 Nullable</li>
</ol>
<p>这两个运算符是相互对应的。您使用这两个运算符限定变量,然后编译器根据您的限定来确保 <code>null</code> 安全性。</p>
<h3 id="-运算符的用法"><code>?</code> 运算符的用法</h3>
<ol>
<li>Nullable:<code>string? x;</code>
<ul>
<li><code>x</code> 是引用类型,因此默认是<em>不可以为 <code>null</code> 的</em>。</li>
<li>我们使用 <code>?</code> 运算符将其改为<em>可以为 <code>null</code> 的</em>。</li>
<li><code>x = null;</code> 赋值正常,没有警告。</li>
</ul>
</li>
<li>Non-Nullable:<code>string y;</code>
<ul>
<li><code>y</code> 是引用类型,因此默认是<em>不可以为 <code>null</code> 的</em>。</li>
<li><code>y = null;</code> 赋值会产生一个警告,因为您给一个声明为不支持 <code>null</code> 的变量分配了一个 <code>null</code> 值。</li>
</ul>
</li>
</ol>
<p>如下图:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010638802-823892295.png" alt="nullable enable warning y" loading="lazy"></p>
<h3 id="-运算符的用法-1"><code>!</code> 运算符的用法</h3>
<pre><code class="language-csharp">string x;
string? y = null;
</code></pre>
<ol>
<li><code>x = y;</code>
<ul>
<li>非法!警告:将 null 文本或可能的 null 值转换为不可为 null 类型(<code>y</code> 可能为 <code>null</code>)。</li>
<li>赋值运算符 <code>=</code> 左边是<em>不可以为 <code>null</code> 的</em>,但右边是<em>可以为 <code>null</code> 的</em>。</li>
</ul>
</li>
<li><code>x = y!;</code>
<ul>
<li>合法!</li>
<li>赋值运算符 <code>=</code> 左右两边都是<em>不可以为 <code>null</code> 的</em>。</li>
<li>因为 <code>y!</code> 使用了 <code>!</code> 运算符到 <code>y</code>,使得右边也变成了<em>不可以为 <code>null</code> 的</em>,所以赋值没有问题。</li>
</ul>
</li>
</ol>
<p>如下图:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210111010716733-160026290.png" alt="nullable enable warning y 2" loading="lazy"></p>
<blockquote>
<p>⚠️ <strong>警告:</strong> <code>null</code> 包容运算符 <code>!</code> 仅在类型系统级别关闭编译器检查;在运行时,该值仍然可能是 <code>null</code>。</p>
</blockquote>
<h2 id="这是反模式的">这是反模式的</h2>
<p><strong>C# 编程时应该尽量避免使用 <em><code>null</code> 包容运算符 <code>!</code></em>。</strong></p>
<p>有一些有效的使用场景(在下面会介绍),比如单元测试,使用这个运算符是适合的。不过,在 99% 的情况下,使用替代解决方案会更好。请不要只是为了取消警告,而在代码中打几十个 <code>!</code>。要想清楚您的场景是否真的值得使用它。</p>
<blockquote>
<p>💡 可以使用,但要小心使用。如果没有实际的目的或使用场景,请不要使用它。</p>
</blockquote>
<p><code>null</code> 包容运算符 <code>!</code> 抵消了您获得的编译器保证的 <code>null</code> 安全性的作用!</p>
<p><strong>使用 <code>!</code> 运算符将导致很难发现 bug。</strong>如果您定义了一个标记为<em>不可以为 <code>null</code> 的</em>属性,您也就假定了可以安全地使用它。但是在运行时,您却突然遇到 <code>NullReferenceException</code> 异常而挠头,因为一个值<em>在用 <code>!</code> 绕过了编译器检查后,实际上却变成了 <code>null</code></em>,这不是给自己添麻烦吗?</p>
<p>既然这样,那么,</p>
<h3 id="为什么--运算符会存在">为什么 <code>!</code> 运算符会存在?</h3>
<ul>
<li>在某些边缘情况下,编译器无法检测到<em>可以为 null 的值</em>实际上是不为 <code>null</code> 的。</li>
<li>使遗留代码库迁移更容易。</li>
<li>在某些情况下,您根本不关心某些内容是否为 <code>null</code>。</li>
<li>在进行单元测试时,您可能想要检查传递 <code>null</code> 时的代码行为。</li>
</ul>
<p>接下来,我们继续看下:</p>
<h2 id="null-是什么意思呢"><code>null!</code> 是什么意思呢?</h2>
<p><code>null!</code> 是在告诉编译器 <code>null</code> 不是 <code>null</code> 值,这听起来很怪异,是不是?</p>
<p>实际上,它和上面例子中的 <code>y!</code> 一样。<strong>它只是看起来挺怪异,因为它将该运算符用在了 <code>null</code> 字面量上</strong>,但概念是一样的。</p>
<p>我们再来看一下文章开头提到的 fluss 源码中的一行代码:</p>
<pre><code class="language-csharp">/// <summary>
/// 所属的博客。
/// </summary>
public BlogSite BlogSite { get; set; } = null!;
</code></pre>
<p>这行代码定义了一个名称为 <code>BlogSite</code>、类型为 <code>BlogSite</code> 的<em>不可以为 <code>null</code> 的</em>类属性。因为它是<em>不可以为 <code>null</code> 的</em>,因此单从技术上讲,很明显它是不可以被赋值为 <code>null</code>的。</p>
<p>但是,您可以通过使用 <code>!</code> 运算符,将 <code>BlogSite</code> 属性赋值为 <code>null</code>。因为,<strong>就编译器所关心的 <code>null</code> 安全性而言,<code>null!</code> 不是 <code>null</code>。</strong></p>
<h2 id="总结">总结</h2>
<p>看到这里,想必您肯定已经明白了 <code>null!</code> 是什么意思,也学会了 <code>null</code> 包容运算符 <code>!</code> 的概念、由来和用法。但是正如我在文中提到的那样,编程时应该尽量避免使用 <code>!</code>,因为它抵消了您本可以获得的编译器保证的 <code>null</code> 安全性;而且,这种写法阅读起来有点让人费解。</p>
<hr>
<h2 id="有朋友说文章内容不太容易看懂我补充两张图帮助理解一下">有朋友说文章内容不太容易看懂,我补充两张图帮助理解一下:</h2>
<p>C# 8.0之前:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210115145731843-671673598.png" alt="null forgiving operator 1" loading="lazy"></p>
<p>C# 8.0之后:</p>
<p><img src="https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210115152127480-198371875.png" alt="null forgiving operator 2" loading="lazy"></p>
<hr>
<p><strong>参考:</strong></p>
<ul>
<li>https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references</li>
<li>https://stackoverflow.com/questions/54724304/what-does-null-statement-mean</li>
<li>https://www.cnblogs.com/cmt/p/14217355.html</li>
<li>https://www.meziantou.net/csharp-8-nullable-reference-types.htm</li>
</ul>
<br>
<blockquote>
<p>作者 : 技术译民<br>
出品 : 技术译站</p>
</blockquote>
</div>
<div id="MySignature" role="contentinfo">
<div><p style="font-size: 14px; font-family: '微软雅黑';font-weight: 400; padding: 0 0 5px 2px;color:#888;">© 转载请标明出处 https://www.cnblogs.com/ittranslator</p></div>
<div style="text-align: center;max-width: 280px;margin: 10px auto;">
<p style="font-size: 18px; font-weight: 600; color: rgba(0, 0, 0, 1); padding-top: 6px; padding-bottom: 6px; border-bottom: 1px dashed rgba(119, 119, 255, 1)">不做标题党,只分享技术干货
</p><p style="font-size: 13px; font-weight: 400; padding-top: 6px; padding-bottom: 0px;color:rgb(66,66,166);">公众号『技术译站』,<b>欢迎扫码关注</b></p>
<img style="width: 215px;" src="https://img2020.cnblogs.com/blog/2074831/202006/2074831-20200628152541133-1651846078.jpg" alt="">
</div><br><br>
来源:https://www.cnblogs.com/ittranslator/p/14260348.html
頁:
[1]