注意
本文介绍可为 null 的引用类型。 还可以声明可为 null 的值类型。
在 可为 null 感知上下文中的代码中使用可为 null 的引用类型。 可为 null 的引用类型、null 静态分析警告和 null 包容运算符是可选的语言功能。 默认情况下,所有选项都处于关闭状态。 通过使用生成设置或在代码中使用杂注来控制项目级别的 可为 null 上下文 。
C# 语言参考记录了最近发布的 C# 语言版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。
本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。
小窍门
若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。
重要
所有项目模板都为项目启用可为 null 的上下文。 使用早期模板创建的项目不包含此元素,并且除非在项目文件中启用这些功能或使用 pragma,否则这些功能将关闭。
在可为 null 的感知上下文中:
- 必须使用非 null 值初始化引用类型的
T变量,并且永远不能分配可能null的值。 - 可以使用或赋值
T?初始化引用类型的nullnull变量,但必须在取消引用之前对其进行null检查。 - 将 null forgiving 运算符应用于类型的
m变量T?时,如中所示m!,该变量被视为非 null。
编译器使用上述规则强制实施不可为 null 引用类型和 T 可为 null 引用类型 T? 之间的区别。
T 类型的变量和 T? 类型的变量是相同的 .NET 类型。 下面的示例声明了一个不可为 null 的字符串和一个可为 null 的字符串,然后使用 null 包容运算符将一个值分配给不可为 null 的字符串:
string notNull = "Hello";
string? nullable = default;
notNull = nullable!; // null forgiveness
变量 notNull 和 nullable 两者都使用类型 String 。 由于不可为 null 和可为 null 的类型都使用相同的类型,因此不能在多个位置使用可为 null 的引用类型。 通常,不能将可以为 null 的引用类型用作基类或实现的接口。 不能在任何对象创建或类型测试表达式中使用可为 null 的引用类型。 不能将可以为 null 的引用类型用作成员访问表达式的类型。 下面的示例说明了这些构造:
public MyClass : System.Object? // not allowed
{
}
var nullEmpty = System.String?.Empty; // Not allowed
var maybeObject = new object?(); // Not allowed
try
{
if (thing is string? nullableString) // not allowed
Console.WriteLine(nullableString);
} catch (Exception? e) // Not Allowed
{
Console.WriteLine("error");
}
可为 null 的引用和静态分析
上一部分中的示例说明了可为 null 的引用类型的性质。 可为 null 的引用类型不是新的类类型,而是对现有引用类型的注释。 编译器使用这些注释来帮助你查找代码中潜在的 null 引用错误。 不可为 null 的引用类型和可为 null 的引用类型在运行时没有区别。 编译器不会为不可为 null 的引用类型添加任何运行时检查。 这有利于编译时分析。 编译器将生成警告,帮助你查找和修复代码中潜在的 null 错误。 你需声明意向,如果代码违反该意向,编译器会发出警告。
重要
可以为 null 的引用注释不会引入行为更改,但其他库可能会使用反射为可为 null 和不可为 null 的引用类型生成不同的运行时行为。 值得注意的是,Entity Framework Core 会读取可为 null 的属性。 它将可为 null 的引用解释为“可选”值,而将不可为 null 的引用解释为“必需”值。
在可为 null 的上下文中,编译器对任何引用类型的变量(可为 null 的和不可为 null 的)执行静态分析。 编译器会跟踪每个引用变量的 null-state,即 not-null 或 maybe-null。 不可为 Null 的引用的默认状态为 not-null。 可为 Null 的引用的默认状态为 maybe-null。
不可为 Null 的引用类型在取消引用时应该始终是安全的,因为它们的 null-state 是 not-null。 若强制执行该规则,如果不可为 null 的引用类型没有初始化为非 null 值,编译器将发出警告。 必须在其中分配声明它们的局部变量。 必须在字段初始化表达式或每个构造函数中为每个字段分配 not-null 值。 如果将不可为 Null 的引用分配给状态为 maybe-null 的引用,编译器会发出警告。 通常,不可 为 null 的引用不为 null ,在取消引用这些变量时不会发出任何警告。
注意
如果将 maybe-null 表达式分配给不可为 Null 的引用类型,编译器会生成警告。 然后,编译器会针对该变量生成警告,直到将该变量分配给 not-null 表达式。
可以初始化或分配给 null 可以为 null 的引用类型。 因此,静态分析必须在取消对变量的引用之前确定该变量的状态为 not-null。 如果确定可为 null 的引用 可能为 null,则将其分配给不可为 null 的引用变量将生成编译器警告。 以下类显示了这些警告的示例:
public class ProductDescription
{
private string shortDescription;
private string? detailedDescription;
public ProductDescription() // Warning! shortDescription not initialized.
{
}
public ProductDescription(string productDescription) =>
this.shortDescription = productDescription;
public void SetDescriptions(string productDescription, string? details=null)
{
shortDescription = productDescription;
detailedDescription = details;
}
public string GetDescription()
{
if (detailedDescription.Length == 0) // Warning! dereference possible null
{
return shortDescription;
}
else
{
return $"{shortDescription}\n{detailedDescription}";
}
}
public string FullDescription()
{
if (detailedDescription == null)
{
return shortDescription;
}
else if (detailedDescription.Length > 0) // OK, detailedDescription can't be null.
{
return $"{shortDescription}\n{detailedDescription}";
}
return shortDescription;
}
}
以下代码段显示了编译器在使用此类时发出警告的位置:
string shortDescription = default; // Warning! non-nullable set to null;
var product = new ProductDescription(shortDescription); // Warning! static analysis knows shortDescription maybe null.
string description = "widget";
var item = new ProductDescription(description);
item.SetDescriptions(description, "These widgets will do everything.");
前面的示例演示编译器的静态分析如何确定引用变量的 null-state。 编译器对 null 检查和分配应用语言规则以通知其分析。 编译器无法对方法或属性的语义进行假设。 如果调用执行 null 检查的方法,则编译器无法得知这些方法会影响变量的 null-state。 可以向 API 添加属性,以通知编译器参数和返回值的语义。 .NET 库中的许多常见 API 都具有这些属性。 例如,编译器正确地将 IsNullOrEmpty 解释为空值检查。 有关应用于 null-state 静态分析的属性的详细信息,请参阅有关可为 Null 的属性的文章。
可为空上下文
可以为 null 的上下文确定编译器如何处理可为 null 的引用类型注释,以及它在静态 null 状态分析期间生成的警告。 可空上下文包含两个标志:批注 设置和警告设置。
对于现有项目,默认禁用了注释和警告设置。 从 .NET 6(C# 10)开始,这两个标志在 新 项目中默认启用。 使用两个不同的标志来设置可为空的上下文的原因是:为了更轻松地迁移在引入可为空的引用类型之前开发的大型项目。
对于小型项目,可以启用可为 null 的引用类型、修复警告并继续。 但是,对于大型项目和多项目解决方案,此过程可能会生成大量警告。 可以使用 pragma 在开始使用可为 null 的引用类型时逐文件启用可为 null 的引用类型。 在现有代码库中,防止引发 System.NullReferenceException 的新功能在启用后可能会导致服务中断:
- 所有显式类型引用变量都均解释为不可为 null 引用类型。
- 泛型中
class约束的含义已更改为非可空引用类型。 - 由于这些新规则,将生成新警告。
可为 null 注释上下文决定了编译器的行为。 可为空上下文设置有四种组合:
-
两者都禁用:代码是 nullable-oblivious。
禁用与启用可为 null 引用类型之前的行为匹配,但新语法生成警告而不是错误。
- 禁用可为 null 警告。
- 所有引用类型变量都是可为 null 引用类型。
- 使用
?后缀来声明可为 null 引用类型会生成警告。 - 可以使用 null 容忍运算符
!,但它不起任何作用。
-
均启用:编译器启用所有 null 引用分析和所有语言功能。
- 启用所有新的可为 null 警告。
- 可使用
?后缀来声明可为 null 引用类型。 - 没有
?后缀的引用类型变量都是不可为 null 的引用类型。 - null 容忍运算符禁止对可能的
null取消引用发出警告。
-
已启用警告
null时,编译器会执行所有 null 分析并发出警告。- 启用所有新的可为 null 警告。
- 使用
?后缀来声明可为 null 引用类型会生成警告。 - 所有引用类型变量均可为 null。 但是,除非使用 后缀声明成员,否则成员在所有方法的左大括号处都具有非 null 的 null 状态
?。 - 可以使用 null 容忍运算符
!。
-
已启用注释:当代码可能取消引用
null或为可能为 null 的表达式赋予不可为 null 的变量时,编译器不会发出警告。- 禁用所有新的可为 null 警告。
- 可使用
?后缀来声明可为 null 引用类型。 - 没有
?后缀的引用类型变量都是不可为 null 的引用类型。 - 可以使用 null 容忍运算符
!,但它不起任何作用。
可以使用 .csproj 文件中的<Nullable>元素为项目设置可为 null 的批注上下文和可为 null 的警告上下文。 此元素配置编译器如何解释类型的可为 null 性以及它发出的警告。 下表显示了允许的值并汇总了它们指定的上下文。
| 背景 | 取消引用警告 | 赋值警告 | 参考类型 |
? 后缀 |
! 运算符 |
|---|---|---|---|---|---|
disable |
禁用 | 禁用 | 全部可为 null | 生成警告 | 没有作用 |
enable |
已启用 | 已启用 | 不可为 null,除非使用 ? 声明 |
声明可为 null 的类型 | 禁止为可能的 null 赋值显示警告 |
warnings |
已启用 | 不適用 | 所有成员都可为 null,但在方法的左大括号处,成员被视为 not-null | 生成警告 | 禁止为可能的 null 赋值显示警告 |
annotations |
禁用 | 禁用 | 不可为 null,除非使用 ? 声明 |
声明可为 null 的类型 | 没有作用 |
对于已禁用的上下文中编译的代码中的引用类型变量,其为 Null 性未知。 可将 null 文本或 maybe-null 变量分配给 Null 性未知的变量。 但是,nullable-oblivious 变量的默认状态为 not-null。
选择最适合你的项目的设置:
- 对于根据诊断或新功能不想更新的旧项目,请选择“禁用”。
- 选择“警告”,确定代码可能引发 System.NullReferenceException 的位置。 可先处理这些警告,然后修改代码来启用不可为 null 引用类型。
- 选择注释来表达您的设计意图,然后再启用警告。
- 对于希望避免出现 null 引用异常的新项目和活动项目,请选择“启用”。
示例:
<Nullable>enable</Nullable>
还可以使用指令在源代码中的任何位置设置这些相同的标志。 这些指令在迁移大型代码库时最有用。
-
#nullable enable:将注释和警告标志设置为启用。 -
#nullable disable:将批注和警告标志设置为 禁用。 -
#nullable restore:将批注标志和警告标志还原到项目设置。 -
#nullable disable warnings:设置要 禁用的警告标志。 -
#nullable enable warnings:设置要 启用的警告标志。 -
#nullable restore warnings:将警告标志还原到项目设置。 -
#nullable disable annotations:设置要 禁用的批注标志。 -
#nullable enable annotations:设置要 启用的批注标志。 -
#nullable restore annotations:将注释标志还原到项目设置。
对于任何代码行,可设置以下任意组合:
| 警告标志 | 注释标志 | 使用 |
|---|---|---|
| 项目默认 | 项目默认 | 默认 |
| 启用 | 停用 | 修复分析警告 |
| 启用 | 项目默认 | 修复分析警告 |
| 项目默认 | 启用 | 添加类型注释 |
| 启用 | 启用 | 已迁移的代码 |
| 停用 | 启用 | 在修复警告之前注释代码 |
| 停用 | 停用 | 将旧代码添加到已迁移的项目 |
| 项目默认 | 停用 | 很少 |
| 停用 | 项目默认 | 很少 |
这九种组合提供对编译器为代码发出的诊断的精细控制。 你可在正在更新的任何区域中启用更多功能,而不显示尚未准备好解决的其他警告。
重要
全局可为 null 上下文不适用于生成的代码文件。 在这两种策略下,标记为“已生成”的任何源文件的可空上下文将被禁用。 此条件意味着编译器不会在生成的文件中批注任何 API。 编译器不会为生成的文件生成可为 null 的警告。 使用以下四种方式之一将文件标记为生成:
- 在 .editorconfig 中,在应用于该文件的部分中指定
generated_code = true。 - 将
<auto-generated>或<auto-generated/>放在文件顶部的注释中。 它可以位于该注释中的任意行上,但注释块必须是该文件中的第一个元素。 - 文件名以 TemporaryGeneratedFile_ 开头
- 文件名用以 .designer.cs、.generated.cs、.g.cs 或 .g.i.cs 结尾。
生成器可以使用预处理器指令选择加入 #nullable 。
默认情况下,可为空注释和警告标志处于禁用状态。 默认值意味着现有代码编译时无需更改,也无需生成任何新警告。 从 .NET 6 开始,新项目在所有项目模板中包含 <Nullable>enable</Nullable> 元素,将这些标志设置为 enabled。
这些选项提供两种不同的策略来更新现有代码库以使用可为 null 的引用类型。
设置可为 null 的上下文
可以通过两种方式控制可以为 null 的上下文。 在项目级别,添加 <Nullable>enable</Nullable> 项目设置。 在单个 C# 源文件中 #nullable enable ,添加杂注以启用可为 null 的上下文。 有关详细信息,请参阅 设置可为 null 的策略。 在 .NET 6 之前,新项目使用默认值 <Nullable>disable</Nullable>。 从 .NET 6 开始,新项目将在项目文件中包含 <Nullable>enable</Nullable> 元素。
泛 型
使用类型参数 T(作为可为 null 的对应参数)时, T?实际类型参数将确定解释方式 ? 。 请考虑以下泛型声明:
public class Box<T>
{
public T Contents { get; set; }
}
由于类型参数可以代表引用类型或值类型,因此取决于调用方提供的类型参数的含义 T? 。 以下规则描述了在没有任何约束时T解析的内容T?:
- 类型参数是不可为 null 的引用类型。 对于
Box<string>,T是string且T?是string?相应的可为 null 引用类型。 - 类型参数是值类型。 对于
Box<int>,T也是intT?int相同的值类型。 除非类型参数具有struct约束,否则批注对值类型没有影响,在这种情况下T?意味着 Nullable<T> (int?)。 - 类型参数已可为 null。 因此
Box<string?>,T是string?和T?仍然是string?。 不会获得“可为 null 的”类型。
约束 限制允许哪些类型参数。 它们还让编译器推理如何使用 T :
-
where T : class需要不可为 null 的引用类型。Box<string>是允许的;Box<string?>生成警告。 -
where T : class?允许为 null 或不可为 null 的引用类型。 这两者Box<string>均允许使用Box<string?>。 -
where T : struct需要不可为 null 的值类型。Box<int>是允许的;Box<int?>不是。 使用此约束时,在泛型平均值 内,为 < a0 />。 -
where T : notnull需要不可为 null 的引用或值类型。Box<string>并且Box<int>是允许的;Box<string?>生成警告。 -
where T : BaseType需要派生自BaseType的不可为 null 的引用类型。 追加?(where T : BaseType?) 以允许可以为 null 的派生类型。
约束有助于编译器解释如何使用泛型类型参数:
public static T? FirstOrDefault<T>(IEnumerable<T> source)
{
foreach (T item in source)
{
return item;
}
return default;
}
public static void RequireNotNull<T>(T value) where T : notnull
{
ArgumentNullException.ThrowIfNull(value);
}
public static void Generics()
{
string? first = FirstOrDefault<string>([]);
Console.WriteLine(first ?? "<empty>");
RequireNotNull("not null");
}
C# 语言规范
有关详细信息,请参阅 C# 语言规范的可为 Null 引用类型部分。