SuperAPI AI 工具集

csharp-nullable-reference-types

编程与开发

查看项目

更新于 10-15 09:47

技能介绍

用途

本技能用于指导在 C# / .NET 代码库中引入与使用可空引用类型(Nullable Reference Types,NRT)以及 System.Diagnostics.CodeAnalysis 中的可空性属性。适用场景包括:向尚未采用 NRT 的代码库引入 NRT;编写或重构使用 T? / 可空注解的 C# 代码;使用 System.Diagnostics.CodeAnalysis 可空性属性标注 API;设计关注可空性契约的公共/内部 API;封装未标注或旧版 API 以便下游调用方仍能受益于 NRT;审查代码中的 null 状态分析、守卫辅助方法与 field 关键字。

核心目标

  • 通过在签名中显式表达 null 意图,防止运行时的 NullReferenceException。
  • 使用官方可空性属性表达类型系统无法直接表示的契约。
  • 在旧代码库中增量采用 NRT,无需一次性重写。

核心可空性模型

非可空与可空:string 为非可空引用,编译器假定实例永不为 null,赋 null 或可能为 null 的值会产生警告。string? 为可空引用,变量可能为 null,编译器要求解引用前进行 null 检查。示例:string name = "Alice"; name = null; // 警告;string? nickname = null; Console.WriteLine(nickname.Length); // 警告:可能解引用 null。

null 状态流分析:编译器跟踪引用是“肯定非 null”还是“可能为 null”,null 检查与赋值会更新该状态。在可空值解引用前引入显式 null 检查(if (x != null)、is not null、模式匹配),尽早收窄可空性并保持非 null 状态。空条件赋值(C# 14)允许写 customer?.Order = CreateOrder();,右侧仅在接收者非 null 时求值。

null 宽容运算符(!):x! 告诉编译器此处将 x 视为非 null,仅影响分析,不影响运行时行为。仅在真实不变量保证非 null 且编译器无法识别时使用 !,不要将其作为消除警告的通用手段,优先重构控制流、添加属性或正确初始化成员。示例:_customer = LoadCustomerFromOrm()!; // ORM 保证有效状态下非 null。

在抑制警告前重组代码:成功的守卫子句或模式匹配已在当前作用域创建 null 安全区域。在添加 ! 之前,让可空值跨越一次已检查边界,并保持其余代码非可空:用守卫子句或模式匹配尽早收窄;在检查前将可空字段或属性复制到局部变量,避免重复读取改变分析结果;当方法控制流复杂时,可将非 null 路径移入具有非可空参数的局部函数或私有方法;将可空处理保持在边界处,而不是让 T?、重复检查或 ! 散布于实现中。示例:if (order?.Customer is not { } customer) { return; } Console.WriteLine(customer.Name);。不要仅为满足可空性分析而抽取函数;仅当显式非可空函数边界同时能简化庞大或分支复杂的实现时使用。仅当真实外部不变量无法通过控制流、签名或可空性分析属性表达时使用 !。

项目配置

为新代码启用 NRT:<PropertyGroup><Nullable>enable</Nullable></PropertyGroup>。对于旧代码库,可使用文件级指令(#nullable enable、#nullable disable、#nullable enable warnings、#nullable enable annotations)增量启用。将 CS86xx 可空性警告视为重要;可考虑 TreatWarningsAsErrors,或在新建项目中将可空性警告视为错误。完整的增量采用策略、#nullable 指令参考、旧版互操作及已知静态分析限制(数组、default(struct))见 nrt-migration-playbook-reference.md。

API 设计规则(签名)

以下规则适用于公共与内部 API 及模型。

参数:若不允许 null,使用非可空类型并为公共 API 添加运行时守卫,如 ArgumentNullException.ThrowIfNull(recipient);。若允许且有意义的 null,使用 T?、文档说明 null 的解释并实现正确的 null 行为。

返回类型:方法永不返回 null 时用 Customer;可合法返回 null 时用 Customer?(调用方必须检查,编译器强制)。示例:public Customer GetRequiredCustomer(Guid id); // 失败时抛出;public Customer? TryGetCustomer(Guid id); // 失败时返回 null。

属性与字段:遵循与参数、返回类型相同的规则。非可空成员必须在构造函数中初始化,或通过 required 属性配合对象初始化器、字段支持的惰性属性、或标注 [MemberNotNull] 的辅助方法初始化。示例:public required string Id { get; init; } public required Customer Customer { get; init; } public string? Comment { get; init; }。

当契约依赖于输入/输出行为、条件行为或成员初始化时,应用 nullable-attributes-reference.md 中描述的可空性属性。

库的公共 API 兼容性

将可空性注解与可空性分析属性视为发布的 API 契约的一部分。T 与 T? 具有相同 CLR 类型,因此仅注解变更通常二进制兼容,但可能对启用可空性的使用方引入警告,从而构成源破坏;当使用方将警告视为错误时,这些警告往往变成构建失败。发布前审查公共可空性变更,尤其是:将输出从 T 放宽为 T? 或添加 [MaybeNull];将输入从 T? 收紧为 T 或添加 [DisallowNull];更改泛型约束如 class?、class、notnull;更改虚成员、接口、委托及实现上的注解或属性(不匹配会产生编译器警告)。为先前对可空不敏感的 API 添加注解也可能产生相同的源兼容问题。将注解后的表面与上一发布版本比较,用启用可空性的使用方测试,并根据库的兼容策略记录或版本化有意的源破坏变更。

field 关键字(C# 14 / .NET 10)

field 上下文关键字允许编写属性访问器主体而无需声明显式后备字段。这是主要的 NRT 场景(惰性初始化属性),编译器会执行特殊的 null 韧性分析,因此在构造函数中不会产生烦人的 CS8618。示例:public class C { public C() { } string Prop => field ??= GetPropValue(); }。完整的 field 可空性规则(韧性与非韧性 getter、[field: AllowNull, MaybeNull] 逃生舱、setter/构造函数分析)见 nullable-attributes-reference.md。

引用文件

  • nullable-attributes-reference.md:完整的 System.Diagnostics.CodeAnalysis 属性目录——前置条件(AllowNull、DisallowNull)、后置条件(MaybeNull、NotNull)、条件后置条件(NotNullWhen、MaybeNullWhen、NotNullIfNotNull)、辅助方法(MemberNotNull、MemberNotNullWhen)、不可达代码辅助(DoesNotReturn、DoesNotReturnIf)以及 field 关键字可空性规则,每项附意图、模式与代理规则。
  • nrt-migration-playbook-reference.md:增量采用策略、#nullable 指令参考、旧版/未注解 API 互操作、为旧目标框架补充可空性属性(含权衡与添加前确认决策流程)、已知静态分析限制(非可空引用数组、含引用字段的 default(struct))、警告处理及完整生成检查清单。

生成检查清单(摘要)

  • 项目:存在 <Nullable>enable</Nullable>;仅围绕不可避免的旧代码禁用。
  • 类型:必需的参数/返回/属性用非可空;仅当 null 有效且符合预期时用 T?。
  • 初始化:构造函数、required + 对象初始化器、字段支持的惰性 getter 或 [MemberNotNull] 辅助方法。避免 null!,除非作为有文档的逃生舱。
  • null 检查:公共边界处显式守卫;用控制流收窄,仅当能改善复杂代码时抽取非可空辅助方法;仅在明确不变量时用 !。
  • 属性:用于表达类型系统无法表达的契约(见引用文件)。
  • 互操作:信任 BCL/已注解库;封装未注解 API 时添加自己的守卫与属性。
  • 警告:绝不忽略;修复设计或添加属性,而非用 ! 或 #pragma 抑制。
  • 兼容性:对于已发布库,将公共可空性变更审查为潜在的源破坏,并测试启用可空性的使用方。

参考资料

  • 可空引用类型(概述)
  • null 状态静态分析属性
  • 可空性迁移策略
  • 破坏性变更:可空引用类型注解变更
  • 教程:用可空与非可空引用类型表达设计意图
  • C# 14 新增内容(扩展成员、field 关键字 GA、空条件赋值)
  • field 上下文关键字(功能规范)