在启用可为 null 的上下文中,编译器对代码执行静态分析,以确定所有引用类型变量的 null 状态:
- null 状态:静态分析确定变量具有非 null 值。
- 可能-null:静态分析无法确定将变量分配给非 null 值。
这些状态使编译器能够在取消引用 null 值时提供警告,并引发一个 System.NullReferenceException。 这些属性为编译器提供有关参数、返回值和对象成员 的 null 状态 的语义信息。 这些属性阐明了参数的状态和返回值。 当 API 使用此语义信息正确批注时,编译器会提供更准确的警告。
本文提供每个可为 null 引用类型特性的简要说明以及它们的使用方法。
我们从一个示例开始。 假设你的库具有以下检索资源字符串的 API。 此方法最初是在忽略可为 null 的上下文中编译的:
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
前面的示例遵循 .NET 中熟悉的 Try* 模式。 此 API 有两个引用参数:key 和 message。 此 API 具有与这些参数的 null 状态相关的以下规则:
- 调用方不应将
null作为key的参数传递。 - 调用方可以传递值为
null的变量作为message的参数。 - 如果
TryGetMessage方法返回true,则message的值不为 null。 如果返回值为falsenull,则值为messagenull。
key 的规则可以简洁地表示:key 应是不可为 null 的引用类型。
message 参数更复杂。 它允许将 null 变量作为参数,但保证成功时 out 参数不是 null。 对于这些情况,需要使用更丰富的词汇来描述期望。 该NotNullWhen特性描述用于message参数的参数的 null 状态。
注意
添加这些特性将为编译器提供有关 API 规则的更多信息。 在启用了 null 的上下文中编译调用代码时,编译器会在违反这些规则时警告调用方。 这些特性不会启用对实现进行更多检查。
| Attribute | Category | 含义 |
|---|---|---|
| AllowNull | Precondition | 不可为 null 的参数、字段或属性可能为 null。 |
| DisallowNull | Precondition | 可为 null 的参数、字段或属性应永不为 null。 |
| MaybeNull | 后置条件 | 不可为 null 的参数、字段、属性或返回值可能为 null。 |
| NotNull | 后置条件 | 可为 null 的参数、字段、属性或返回值从不为 null。 |
| MaybeNullWhen | 有条件后置条件 | 当方法返回指定 bool 值时,不可为 null 的参数可能为 null。 |
| NotNullWhen | 有条件后置条件 | 当方法返回指定 bool 值时,可为 null 的参数不为 null。 |
| NotNullIfNotNull | 有条件后置条件 | 如果指定参数的自变量不为 null,则返回值、属性或自变量不为 null。 |
| MemberNotNull | 方法和属性帮助程序方法 | 方法返回时,列出的成员不为 null。 |
| MemberNotNullWhen | 方法和属性帮助程序方法 | 当方法返回指定 bool 值时,列出的成员不为 null。 |
| DoesNotReturn | 无法访问的代码 | 方法或属性永远不会返回。 换句话说,它总是引发异常。 |
| DoesNotReturnIf | 无法访问的代码 | 如果关联的 bool 参数具有指定值,则此方法或属性永远不会返回。 |
上述说明是对每个特性的快速参考。 以下各节更详尽地描述了这些属性的行为和含义。
前置条件:AllowNull 和 DisallowNull
请考虑一个从不返回 null 的读/写属性,因为它具有合理的默认值。 调用方在将 null 设置为该默认值时将其传递给 set 访问器。 例如,假设一个消息系统要求在聊天室中输入一个屏幕名称。 如果未提供任何内容,系统将生成一个随机名称:
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;
当你在忽略可为 null 的上下文中编译前面的代码时,一切都是正常的。 启用可为 null 的引用类型后,ScreenName 属性将成为不可为 null 的引用。 调用方不需要检查返回的 null 属性。 但现在将属性设置为 null 将生成警告。 若要支持这种类型的代码,请向属性添加 System.Diagnostics.CodeAnalysis.AllowNullAttribute 特性,如下面的代码所示:
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
可能需要添加一个 using 指令来 System.Diagnostics.CodeAnalysis 使用此指令以及本文中讨论的其他属性。 特性应用于属性,而不是 set 访问器。
AllowNull 特性指定前置条件,并且仅适用于参数。
get 访问器有一个返回值,但没有参数。 因此,AllowNull 特性只适用于 set 访问器。
前面的示例演示了在参数上添加 AllowNull 特性时要查找的内容:
- 该变量的一般约定是它不应为
null,因此需要一个不可为 null 的引用类型。 - 在某些情况下,调用方将
null作为参数传递,但它们不是最常见的用法。
大多数情况下,需要此属性作为属性或inout参数和ref参数。 当变量通常为非 null 时,AllowNull 属性是最佳选择,但需要允许 null 作为前提条件。
将此与使用 DisallowNull 的方案相比:使用此特性可以指定可为 null 引用类型的参数不应为 null。 请考虑一个特性,其中 null 是默认值,但客户端只能将其设置为非 null 值。 考虑下列代码:
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;
前面的代码是表达设计的最佳方式,ReviewComment 可以是 null,但不能设置为 null。 代码可识别为 null 后,你就可以使用 System.Diagnostics.CodeAnalysis.DisallowNullAttribute 向调用方更清楚地表达此概念:
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
在可为 null 的上下文中,ReviewCommentget 访问器可以返回默认值 null。 编译器会警告在访问之前必须进行检查。 此外,它警告调用方,即使它可能是 null,调用方也不应显式地将其设置为 null。
DisallowNull 特性还指定了前置条件,它不影响 访问器。 当你观察到以下特征时,可以使用 DisallowNull 特性:
- 在核心方案中(通常是在首次实例化时),变量可以是
null。 - 变量不应显式设置为
null。
这些情况在忽略 null 的代码中很常见 。 可能是在两个不同的初始化作中设置了对象属性。 可能是某些属性仅在某些异步工作完成后设置。
使用 AllowNull 和 DisallowNull 属性可以指定变量上的前置条件可能与这些变量上的可为 null 注释不匹配。 这些批注提供有关 API 特征的更多详细信息。 此附加信息有助于调用方正确使用 API。 请记住,可使用以下特性指定前提条件:
- AllowNull:不可为 null 的参数可能为 null。
- DisallowNull:可为 null 的参数不应为 null。
后置条件:MaybeNull 和 NotNull
假设你有使用以下签名的方法:
public Customer FindCustomer(string lastName, string firstName)
你可能编写了一个这样的方法,以在找不到名称时返回 null 。
null 清楚地表明未找到记录。 在本例中,你可能会将返回类型从 Customer 更改为 Customer?。 将返回值声明为可为 null 的引用类型可以清楚地指定此 API 的意图:
public Customer? FindCustomer(string lastName, string firstName)
由于泛 型可为空性 所涵盖的原因,该技术可能无法生成与 API 匹配的静态分析。 你可能有一个遵循类似模式的泛型方法:
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
当找不到所需项时,此方法返回 null。 可以通过将 null 注释添加到方法返回来阐明该方法在未找到项目时返回 MaybeNull:
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
前面的代码通知调用方返回值实际上 可以为 null。 它还通知编译器该方法可以返回 null 表达式,即使类型不可为 null。 当你有一个返回其类型参数 T 的实例的泛型方法时,你可以使用 null 属性表示它永远不会返回 NotNull。
还可以指定返回值或者参数不为 null,即使该类型是可为 null 的引用类型。 以下方法是一种帮助器方法,如果其第一个参数为 null,则引发该方法:
public static void ThrowWhenNull(object value, string valueExpression = "")
{
if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}
可以按如下方式调用此例程:
public static void LogMessage(string? message)
{
ThrowWhenNull(message, $"{nameof(message)} must not be null");
Console.WriteLine(message.Length);
}
启用 null 引用类型后,需要确保前面的代码在编译时没有警告。 当方法返回时,value 参数保证不为 null。 但是,可以使用 null 引用调用 ThrowWhenNull。 可以将 value 设为可为 null 的引用类型,并将 NotNull 后置条件添加到参数声明中:
public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
_ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
// other logic elided
前面的代码清楚地表达了现有协定:调用方可以传递具有 null 值的变量,但如果该方法在未引发异常的情况下返回,则保证该参数永远不为 null。
可以使用以下特性指定无条件后置条件:
有条件后置条件:NotNullWhen、MaybeNullWhen 和 NotNullIfNotNull
你可能很熟悉 string 方法 String.IsNullOrEmpty(String)。 当参数为 null 或为空字符串时,此方法返回 true。 这是一种 null 检查格式:如果方法返回 false,调用方不需要 null 检查参数。 若要使这样的方法可识别为 null,需要将参数设置为可为 null 的引用类型,并添加 NotNullWhen 特性:
bool IsNullOrEmpty([NotNullWhen(false)] string? value)
这通知编译器,任何返回值为 false 的代码都不需要 null 检查。 添加特性通知编译器的静态分析,IsNullOrEmpty 执行必要的 null 检查:当它返回 false 时,参数不是 null。
string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.
该方法 String.IsNullOrEmpty(String) 如上一示例中所示。 代码库中可能有类似的方法,用于检查对象的状态是否为 null 值。 编译器无法识别自定义 null 检查方法,需要自行添加批注。 添加属性时,编译器的静态分析知道测试的变量何时检查为 null。
这些特性的另一个用途是 Try* 模式。
ref 和 out 参数的后置条件通过返回值进行通信。 考虑前面显示的这个方法(在一个禁用可为 null 的上下文中):
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
上述方法遵循典型的 .NET 成语:返回值指示是否已 message 设置为所寻求值,或者如果未找到任何消息,则为默认值。 如果方法返回 true,message 的值不为 null;否则,该方法将 message 设置为 null。
在启用可为 null 的上下文中,可以使用 NotNullWhen 属性传达该习惯用法。 注释可为 null 引用类型的参数时,将 message 设为 string? 并添加属性:
bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message is not null;
}
在前面的示例中,message 的值在 TryGetMessage 返回 true 时不为 null。 你应以相同的方式在代码库中注释类似的方法:参数可以等于 null,并且已知在方法返回 true 时不为 null。
你可能还需要一个最终属性。 有时,返回值的 null 状态取决于一个或多个参数的 null 状态。 每当某些参数不 null为 null 时,这些方法将返回非 null 值。 若要正确地注释这些方法,可以使用 NotNullIfNotNull 特性。 请考虑以下方法:
string GetTopLevelDomainFromFullUrl(string url)
如果 url 参数不为 null,则输出不是 null。 启用可为 null 引用后,如果 API 可以接受 null 参数,则需要添加更多批注。 可以注释返回类型,如以下代码所示:
string? GetTopLevelDomainFromFullUrl(string? url)
这同样有效,但通常强制调用方实施额外的 null 检查。 协定是,只有当参数 null 是 url 时,返回值才会是 null。 若要表达该协定,你需要注释此方法,如以下代码所示:
[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)
上一个示例为 nameof 参数使用 url 运算符。 返回值和参数都用 ? 进行了注释,这表明两者都可以是 null。 该属性进一步阐明了当参数null不为 null 时url返回值不为 null。
可以使用以下特性指定条件的后置条件:
-
MaybeNullWhen:当方法返回指定
bool值时,不可为 null 的参数可以为 null。 -
NotNullWhen:当方法返回指定
bool值时,可为 null 的参数不为 null。 - NotNullIfNotNull:如果指定参数的参数不为 null,则返回值不为 null。
帮助程序方法:MemberNotNull 和 MemberNotNullWhen
在将常见代码从构造函数重构为帮助程序方法时,这些属性会指定意向。 C# 编译器分析构造函数和字段初始值设定项,以确保在每个构造函数返回之前初始化所有不可为 null 的引用字段。 然而,C# 编译器不会通过所有帮助程序方法跟踪字段赋值。 当字段没有在构造函数中直接初始化,而在帮助程序方法中初始化时,编译器会发出警告 CS8618。 可以将 MemberNotNullAttribute 添加到方法声明中,并指定在方法中初始化为非 NULL 值的字段。 例如,考虑以下情况:
public class Container
{
private string _uniqueIdentifier; // must be initialized.
private string? _optionalMessage;
public Container()
{
Helper();
}
public Container(string message)
{
Helper();
_optionalMessage = message;
}
[MemberNotNull(nameof(_uniqueIdentifier))]
private void Helper()
{
_uniqueIdentifier = DateTime.Now.Ticks.ToString();
}
}
可以指定多个字段名称作为 MemberNotNull 特性构造函数的参数。
MemberNotNullWhenAttribute 有 bool 参数。 在帮助程序方法返回指明帮助程序方法是否初始化了字段的 MemberNotNullWhen 的情况下,可以使用 bool。
调用的方法引发时停止可为 null 分析
某些方法(通常是异常帮助程序或其他实用工具方法)始终通过引发异常退出。 或者,帮助程序根据布尔参数的值引发异常。
在第一种情况下,可以将 DoesNotReturnAttribute 特性添加到方法声明中。 编译器的 null 状态分析不会检查调用带有 注释的方法之后的方法中的任何代码。 请考虑此方法:
[DoesNotReturn]
private void FailFast()
{
throw new InvalidOperationException();
}
public void SetState(object containedField)
{
if (containedField is null)
{
FailFast();
}
// containedField can't be null:
_field = containedField;
}
调用 FailFast 后,编译器不会发出任何警告。
在第二种情况下,需将 System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute 特性添加到方法的布尔参数中。 你可以修改前面的示例,如下所示:
private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
if (isNull)
{
throw new InvalidOperationException();
}
}
public void SetFieldState(object? containedField)
{
FailFastIf(containedField == null);
// No warning: containedField can't be null here:
_field = containedField;
}
当参数的值与 DoesNotReturnIf 构造函数的值匹配时,编译器不会在该方法之后执行任何 null 状态分析。
总结
添加可为 null 的引用类型提供了一个初始词汇表,用于描述 API 对可能为 null 的变量的期望。 这些特性提供了更丰富的词汇来将变量的 null 状态描述为前置条件和后置条件。 这些特性更清楚地描述了你的期望,并为使用 API 的开发人员提供了更好的体验。
在为可为 null 的上下文中更新库时,添加这些特性可指导用户正确使用 API。 这些特性有助于你全面描述参数和返回值的 null 状态。
- AllowNull:不可为 null 的字段、参数或属性可能为 null。
- DisallowNull:可为 null 的字段、参数或属性应永不为 null。
- MaybeNull:不可为 null 的字段、参数、属性或返回值可能为 null。
- NotNull:可为 null 的字段、参数、属性或返回值永远不会为 null。
-
MaybeNullWhen:当方法返回指定
bool值时,不可为 null 的参数可能为 null。 -
NotNullWhen:当方法返回指定
bool值时,可为 null 的参数不为 null。 - NotNullIfNotNull:如果指定参数的自变量不为 null,则参数、属性或返回值不为 null。
- DoesNotReturn:方法或属性永远不会返回。 换句话说,它总是引发异常。
-
DoesNotReturnIf:如果关联的
bool参数具有指定值,则此方法或属性永远不会返回。