使用可为 Null 的引用类型更新代码库以改进 null 诊断警告

使用可为 Null 的引用类型可以声明是否应为引用类型的变量分配 null 值。 当代码可能取消引用 null 时会出现的编译器的静态分析和警告是此功能的最重要优势。 启用后,编译器会生成警告,帮助你避免在代码运行时引发 System.NullReferenceException

如果代码库相对较小,你可以在项目中启用该功能,解决警告,并享受改进后的诊断所带来的好处。 经过一段时间后,更大的代码库可能需要通过结构化程度更高的方法来解决警告,这就需要在解决不同类型的文件中的警告时为某些代码库启用该功能。 本文介绍了更新代码库的不同策略,以及与这些策略相关的折衷方案。 在开始迁移之前,请阅读可为 Null 的引用类型的概念性概述。 此文涵盖了编译器的静态分析、maybe-null 和 not-null 的 null-state 值,以及可为 Null 的注释 。 熟悉这些概念和术语后,便可以开始迁移代码。

规划迁移

无论以哪种方式更新代码库,目标都是在项目中启用可为 Null 的警告和可为 Null 的注释。 实现该目标后,项目中就会使用 <nullable>Enable</nullable> 设置。 无需任何预处理器指令即可调整其他位置的设置。

第一种选择是设置项目的默认值。 选项包括:

  1. 可为 null 的 disable 为默认值:如果你不向项目文件添加 Nullable 元素,则 disable 是默认值。 如果你不主动向代码库添加新文件,请使用此默认值。 主要活动是更新库以使用可为 Null 的引用类型。 使用此默认值意味着在更新每个文件的代码时需要向其添加一个可为 null 的预处理器指令。
  2. 可为 null 的 enable 为默认值:如果你正在积极开发新功能,请设置此默认值。 你希望所有新代码都受益于可为 Null 的引用类型和可为 Null 的静态分析。 使用此默认值意味着必须在每个文件的顶部添加 #nullable disable。 处理每个文件中的警告时,你将删除这些预处理器指令。
  3. 可为 null 的警告为默认值:请为两阶段迁移选择此默认值。 在第一阶段解决警告。 在第二阶段,启用注释以声明变量的预期 null 状态。 使用此默认值意味着必须在每个文件的顶部添加 #nullable disable
  4. 可为 null 的注释为默认值。 在解决警告之前注释代码。

启用可为 null 的引用类型作为默认值会增加将预处理器指令添加到每个文件所需的前期工作量。 优点在于,添加到项目的每个新代码文件都支持可为 null 引用类型。 任何新的代码都可识别可为 null 引用类型;只须更新现有代码。 如果库稳定并且开发重点是采用可为 Null 的引用类型,则禁用可为 Null 的引用类型作为默认值效果更好。 在你注释 API 时,将启用可为 null 引用类型。 完成后,将对整个项目启用可为 null 引用类型。 创建新文件时,必须添加预处理器指令并使其能够感知可为 null 的引用类型。 如果团队中任何开发人员忘记了这一点,则需要将所有代码设置为可识别可为 null 引用类型,这样,新代码现将变为积压工作 (backlog)。

选择哪种策略取决于项目中正在进行多少项活动开发。 项目越成熟稳定,第二种策略的效果越好。 开发的功能越多,第一种策略的效果越好。

重要

全局可为空上下文不适用于生成的代码文件。 在这两种策略下,都会针对标记为“已生成”的任何源文件禁用可为空上下文。 这意味着生成的文件中的所有 API 都没有批注。 可采用四种方法将文件标记为“已生成”:

  1. 在 .editorconfig 中,在应用于该文件的部分中指定 generated_code = true
  2. <auto-generated><auto-generated/> 放在文件顶部的注释中。 它可以位于该注释中的任意行上,但注释块必须是该文件中的第一个元素。
  3. 文件名以 TemporaryGeneratedFile_ 开头
  4. 文件名用以 .designer.cs、.generated.cs、.g.cs 或 .g.i.cs 结尾 。

生成器可以选择使用 #nullable 预处理器指令。

了解上下文和警告

启用警告和注释可以控制编译器如何查看引用类型和为 Null 性。 每个类型具有以下三种为 Null 性之一:

  • 未知:如果已禁用注释上下文,则所有引用类型的为 Null 性为未知 。
  • 不可为 Null:如果已启用注释上下文,则不带注释的引用类型 C 不可为 Null 。
  • 可为 Null:如果已禁用注释上下文,则带注释的引用类型 C? 可为 Null,但可能会发出警告 。 如果已启用注释上下文,则使用 var 声明的变量可为 Null。

编译器基于该为 Null 性生成警告:

  • 如果为不可为 Null 的类型分配了潜在的 null 值,则这些类型会导致出现警告。
  • 当可为 Null 的类型的值为 maybe-null 时,取消引用这些类型会导致出现警告 。
  • 当未知类型的值为 maybe-null 并已启用警告上下文时,取消引用这些类型会导致出现警告 。

每个变量具有默认的可为 Null 状态,具体取决于它的为 Null 性:

  • 可为 Null 的变量的默认 null-state 为 maybe-null 。
  • 不可为 Null 的变量的默认 null-state 为 not-null 。
  • 可为 Null 的未知变量的默认 null-state 为 not-null 。

在启用可为 Null 的引用类型之前,代码库中的所有声明都是可为 Null 的未知类型。 知道这一点很重要,因为这意味着所有引用类型的默认 null-state 为 not-null 。

解决警告

如果你的项目使用 Entity Framework Core,请阅读使用可为 Null 的引用类型中的相关指导。

开始迁移时,首先应该只启用警告。 所有声明都保持为不可为 Null 的未知类型,但在其 null-state 更改为 may-null 之后,取消引用某个值时会出现警告 。 解决这些警告时,需要根据其他位置的为 Null 性进行检查,然后代码库的可复原性会变得更高。 若要了解适用于不同情况的具体方法,请参阅有关解决可为 Null 的警告的方法的文章。

在继续处理其他代码之前,可以解决警告并在每个文件或类中启用注释。 但是,在启用类型注释之前解决上下文为警告时生成的警告通常更有效。 这样,在解决第一组警告之前,所有类型均为未知。

启用类型注释

解决第一组警告后,可以启用注释上下文。 这会将引用类型从“未知”更改为“不可为 Null” 。 使用 var 声明的所有变量均可为 Null。 此项更改通常会引入新警告。 解决编译器警告的第一步是在参数和返回类型上使用 ? 注释,以指示参数或返回值何时可为 null。 执行此任务时,目标不只是修复警告。 更重要的目标是让编译器了解潜在 null 值的意图。

特性扩展类型注释

为表示有关变量的 null 状态的附加信息,已添加多个特性。 对于所有参数和返回值,API 的规则可能比 not-null 或 maybe-null 更复杂 。 许多 API 对于变量何时可以或不可以为 null 有更复杂的规则。 在这些情况下,可使用属性来表示这些规则。 可以在有关影响可为 Null 分析的属性的文章中找到描述 API 语义的属性。

后续步骤

在启用注释并已解决所有警告后,可将项目的默认上下文设置为 enabled。 如果你在代码中为可为 Null 的注释或警告上下文添加了任何 pragma,可以将其删除。 你可能会不时地看到新警告。 可以编写引入警告的代码。 可以更新可为 null 的引用类型的库依赖项。 这些更新会将该库中的类型从“可为 Null 的未知类型”更改为“不可为 Null”或“可为 Null” 。

还可以通过关于 C# 中可为 Null 的安全性的学习模块了解这些概念。