在为 null 性未知的上下文中,所有引用类型都可为 null。 可为 null 引用类型指的是已知可为 null 的上下文中引入的一组功能,可用于最大程度地降低代码导致运行时引发
System.NullReferenceException
的可能性。 可为 null 引用类型包括三项功能,可帮助避免这些异常,包括将引用类型显式标记为可为 null 的功能:
经过优化的静态流分析,用于在取消引用变量之前确定其是否为
null
。
属性,用于注释 API 以便流分析确定 null 状态。
变量注释,可供开发人员用于显式声明变量的预期 null 状态。
对现有项目默认禁用 Null 状态分析和变量注释,这意味着所有引用类型仍为可为 null。 从 .NET 6 开始,默认情况下会为新项目启用这些功能。 有关通过声明
可为 null 注释上下文
来启用这些功能的信息,请参阅
可为 null 上下文
。
本文的其余部分介绍了当你的代码可能取消引用
null
值时,这三个功能区域如何生成警告。 取消引用变量意味着使用
.
(点)运算符访问其成员之一,如下例所示:
string message = "Hello, World!";
int length = message.Length; // dereferencing "message"
取消引用值为 null
的变量时,运行时会引发 System.NullReferenceException。
还可以通过关于 C# 中可为 Null 的安全性的学习模块了解这些概念。
null 状态分析
Null 状态分析跟踪引用的 null 状态。 代码可能取消引用 null
时,此静态分析会发出警告。 你可处理这些警告,从而最大程度地减少运行时引发 System.NullReferenceException 的次数。 编译器使用静态分析来确定变量的 null 状态。 变量为“非 null”或“可能为 null” 。 编译器通过两种方式确定变量是否非 null:
已为该变量分配一个已知非 NULL 的值。
已检查该变量是否为 null
,并且该变量自该检查后未进行过修改。
编译器未确定为非 null 的任何变量均视为“可能为 null” 。 如果意外取消引用 null
值,分析会发出警告。 编译器根据 null 状态生成警告。
变量为非 null 时,可安全地取消引用该变量。
变量可能为 null 时,必须先检查该变量,确保其不为 null
,然后才能取消引用它。
请考虑以下示例:
string message = null;
// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");
var originalMessage = message;
message = "Hello, World!";
// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");
// warning!
Console.WriteLine(originalMessage.Length);
在上例中,编译器在打印第一条消息时确定 message
是否可能为 null。 对于第二条消息,没有警告。 originalMessage
可能为 null,因此最后一行代码发出警告。 下面的示例演示了一个更实际的用途,即遍历节点树直到根,并在遍历过程中处理每个节点:
void FindRoot(Node node, Action<Node> processNode)
for (var current = node; current != null; current = current.Parent)
processNode(current);
上述代码不会因取消引用变量 current
而生成任何警告。 静态分析确定当 current
可能为 null 时永不会被取消引用。 访问 current.Parent
以及将 current
传递给 ProcessNode
操作之前,会检查变量 current
是否为 null
。 上述示例演示了编译器如何在初始化、分配或与 null
比较时确定局部变量的 null 状态。
null 状态分析不会跟踪到调用的方法。 因此,构造函数调用的常见帮助程序方法中初始化的字段将使用以下模板生成警告:
在退出构造函数时,不可为 null 的属性“name”必须包含非 null 值。
可以通过以下两种方式之一消除这些警告:帮助程序方法上的构造函数链接或可以为 null 的属性。 下面的代码就是删除两种空格的示例。 Person
类使用由所有其他构造函数调用的通用构造函数。 Student
类具有使用 System.Diagnostics.CodeAnalysis.MemberNotNullAttribute 特性进行批注的帮助程序方法:
using System.Diagnostics.CodeAnalysis;
public class Person
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string firstName, string lastName)
FirstName = firstName;
LastName = lastName;
public Person() : this("John", "Doe") { }
public class Student : Person
public string Major { get; set; }
public Student(string firstName, string lastName, string major)
: base(firstName, lastName)
SetMajor(major);
public Student(string firstName, string lastName) :
base(firstName, lastName)
SetMajor();
public Student()
SetMajor();
[MemberNotNull(nameof(Major))]
private void SetMajor(string? major = default)
Major = major ?? "Undeclared";
C# 10 中添加了对明确赋值和 Null 状态分析的许多改进。 升级到 C# 10 后,便会发现误报的可为 Null 警告更少。 可以详细了解明确赋值改进的功能规范中的改进。
可为 null 的状态分析和编译器生成的警告有助于通过取消引用 null
来避免程序错误。 有关可为 null 的警告的文章提供了用于更正代码中可能看到的警告的技术。
API 签名上的属性
null 状态分析需要开发人员的提示才能理解 API 的语义。 某些 API 提供 null 检查,它们应将变量的 null 状态从“可能为 null”更改为“非 null” 。 其他 API 返回非 null 或可能为 null 的表达式,具体取决于输入参数的 null 状态 。 例如,考虑显示消息的以下代码:
public void PrintMessage(string message)
if (!string.IsNullOrWhiteSpace(message))
Console.WriteLine($"{DateTime.Now}: {message}");
根据检查,所有开发人员都认为此代码安全,不应生成警告。 编译器不知道 IsNullOrWhiteSpace
提供 null 检查。 IsNullOrWhitespace
返回 false,
时,字符串的 null 状态 应为“非 null”。 IsNullOrWhitespace
返回 true
时,null 状态未改变。 在上例中,签名包含 NotNullWhen
以指示 message
的 null 状态:
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);
属性详细说明了用于调用成员的对象实例的参数、返回值和成员的 null 状态。 若要详细了解每个属性,可查看关于可为 null 的引用属性的语言参考文章。 在 .NET 5 中,.NET 运行时 API 均带有注释。 可通过注释 API 提供有关参数和返回值的 null 状态的语义信息来优化静态分析。
可为 null 的变量注释
null 状态分析为大多数变量提供可靠的分析。 编译器需要你提供有关成员变量的更多信息。 编译器无法假定对公共成员的访问顺序。 可按任意顺序访问任何公共成员。 可使用任何可访问的构造函数来初始化对象。 如果某个成员字段曾设为 null
,则编译器必须在每个方法开始时假定其 null 状态是“可能为 null” 。
可使用能够声明变量是可为 null 引用类型还是不可为 null 引用类型的注释 。 这些注释对变量的 null 状态进行重要声明:
引用不应为 null。 不可为 Null 的引用变量的默认状态为 not-null。 编译器会强制执行规则,确保即使不先检查变量是否为 null,也能安全地取消引用这些变量: