了解为 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 之前,只有引用类型可为空。 int
或 DateTime
之类的值类型不能为 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
在上面的示例中:
first
是null
,因为声明了引用类型string
,但未进行赋值。second
在声明时被赋予string.Empty
。 对象从不具有null
赋值。third
是0
,尽管未被赋予。 它是一个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
在上面的示例中:
first
是null
,因为可为空值类型未初始化。second
在声明时被赋予null
。third
是null
,因为Nullable<int>
的default
值为null
。fourth
是0
,因为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 包容运算符 (!
) 为代码添加注释。
此模块的作用域为 disable
或 enable
可为空上下文。 有关详细信息,请参阅可为空引用类型:可为空上下文。
启用可为空引用类型
在 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
示例,它在可为空上下文中分析时有两个警告:
FooBar fooBar = null;
行对null
赋值有一个警告:C# 警告 CS8600:将 null 文本或可能的 null 值转换为不可为 null 的类型。_ = fooBar.ToString();
行还有一个警告。 这次涉及到fooBar
可能为 null 的编译器:C# 警告 CS8602:取消引用可能为 null 的引用。
重要
即使你对所有警告做出反应并消除所有警告,也不能保证 null 安全性。 有一些受限制的情况会通过编译器的分析,但会导致运行时 NullReferenceException
。
总结
在此单元中,已了解如何在 C# 中启用可为空上下文来帮助防范 NullReferenceException
。 在下一个单元中,你将详细了解如何在可为空上下文中明确表达意图。