了解为 Null 性

已完成

如果你是 .NET 开发人员,你可能会遇到 System.NullReferenceException。 当取消引用 null 时(即在运行时评估变量,但变量引用 null 时),则会在运行时出现这种情况。 迄今为止,此异常是 .NET 生态系统中最常见的异常。 null 的创建者(Tony Hoare 爵士)将 null 称为“十亿美元的错误”。

在以下示例中,将 FooBar 变量分配给 null 并立即取消引用,从而出现问题:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

当应用程序的大小和复杂性增加时,作为开发人员发现问题变得更加困难。 发现像这样的潜在错误是工具的一项工作,C# 编译器可以提供帮助。

定义 null 安全性

术语“null 安全性”定义一组特定于可以为 null 的类型的功能,有助于减少可能出现 NullReferenceException 的次数。

考虑到前面的 FooBar 示例,可以通过在取消引用 fooBar 变量之前检查该变量是否为 null 来避免 NullReferenceException

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

为了帮助识别这种情况,编译器可以推断代码的意图并强制实施所需的行为。 但是,这仅适用于启用了可空上下文的情况。 在讨论可为空上下文之前,让我们先介绍一下可能的可以为 null 的类型。

可为 null 的类型

在 C# 2.0 之前,只有引用类型可为空。 intDateTime 之类的值类型不能为 null。 如果这些类型在没有值的情况下被初始化,它们会回退到其 default 值。 对于 int,此值为 0。 对于 DateTime,此值为 DateTime.MinValue

不带初始值的实例化引用类型的工作方式不同。 所有引用类型的 default 值均为 null

请考虑以下 C# 代码片段:

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

在上面的示例中:

  • firstnull,因为声明了引用类型 string,但未进行赋值。
  • second 在声明时被赋予 string.Empty。 对象从不具有 null 赋值。
  • third0,尽管未被赋予。 它是一个 struct(值类型),并且 default 值为 0
  • date 未初始化,但其 default 值为 System.DateTime.MinValue

从 C# 2.0 开始,可以使用 Nullable<T>(或缩写为 T?)定义可空值类型。 这允许值类型可为空。 请考虑以下 C# 代码片段:

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

在上面的示例中:

  • firstnull,因为可为空值类型未初始化。
  • second 在声明时被赋予 null
  • thirdnull,因为 Nullable<int>default 值为 null
  • fourth0,因为 new() 表达式调用 Nullable<int> 构造函数,而且 int 默认为 0

C# 8.0 引入了可为空的引用类型,你可以在其中表达你的意图,即引用类型可能是 null 或始终为非 null。 你可能会想,“我以为所有引用类型都是可为空的!”你想的没错,就是这样。 此功能允许你表达你的意图,然后编译器会尝试强制执行。 相同的 T? 语法表示引用类型可为空。

请考虑以下 C# 代码片段:

#nullable enable

string first = string.Empty;
string second;
string? third;

鉴于前面的示例,编译器会推断你的意图,如下所示:

  • first 永远不为 null,因为它是明确赋值的。
  • second 应永远不为 null,即使它最初为 null 在赋值之前评估 second 会导致编译器警告,因为它未初始化。
  • third 可能是 null 例如,它应指向 System.String,但它可能指向 null。 其中的任何一种变化都是可接受的。 如果取消引用 third 而不事先检查是否不为 null,则编译器会向你发出警告。

重要

为了使用如上所示的可为空引用类型功能,它必须位于可空上下文中。 下一部分将对此进行详细介绍。

可为空上下文

可为空上下文可以对编译器如何解释引用类型变量进行精细控制。 有四种可能的可为空上下文:

  • disable:编译器的行为类似于 C# 7.3 和更早版本。
  • enable:编译器启用所有空引用分析和所有语言功能。
  • warnings:编译器执行所有 null 分析,并在代码可能取消引用 null 时发出警告。
  • annotations:当代码可能取消引用 null 时,编译器不会执行 null 分析或发出警告,但仍可以使用可为空引用类型 ? 和 null 包容运算符 (!) 为代码添加注释。

此模块的作用域为 disableenable 可为空上下文。 有关详细信息,请参阅可为空引用类型:可为空上下文

启用可为空引用类型

在 C# 项目文件 (.csproj) 中,将子 <Nullable> 节点添加到 <Project> 元素(或附加到现有 <PropertyGroup>)。 这会将 enable 可为空上下文应用于整个项目。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

或者,可以使用编译器指令将可为空上下文限定为 C# 文件。

#nullable enable

前面的 C# 编译器指令在功能上等同于项目配置,但其作用域限于它所在的文件。 有关详细信息,请参阅可为空引用类型:可为空上下文 (docs)

重要

默认情况下,在从 .NET 6.0 及更高版本开始的所有 C# 项目模板中的 .csproj 文件中启用可空上下文。

启用可为空上下文时,你会收到新的警告。 考虑前面的 FooBar 示例,它在可为空上下文中分析时有两个警告:

  1. FooBar fooBar = null; 行对 null 赋值有一个警告:C# 警告 CS8600:将 null 文本或可能的 null 值转换为不可为 null 的类型。

    屏幕截图显示 C# 警告 CS8600:将 null 文本或可能的 null 值转换为不可为 null 的类型。

  2. _ = fooBar.ToString(); 行还有一个警告。 这次涉及到 fooBar 可能为 null 的编译器:C# 警告 CS8602:取消引用可能为 null 的引用。

    屏幕截图显示 C# 警告 CS8602:取消引用可能为 null 的引用。

重要

即使你对所有警告做出反应并消除所有警告,也不能保证 null 安全性。 有一些受限制的情况会通过编译器的分析,但会导致运行时 NullReferenceException

总结

在此单元中,已了解如何在 C# 中启用可为空上下文来帮助防范 NullReferenceException。 在下一个单元中,你将详细了解如何在可为空上下文中明确表达意图。