mcpField
C
csharp-nullable-reference-types

csharp-nullable-reference-types

github.com/aaronontheweb开发1.2k138 次安装今天更新
代码仓库

关于 csharp-nullable-reference-types

这是一份关于 C# 可空引用类型的技能指南,涵盖空值契约设计和编译器流分析。内容包括渐进迁移、API 特性、旧代码互操作与兼容性检查。

主要功能

  1. 01可空标注与流分析
  2. 02渐进式 NRT 迁移
  3. 03API 空值契约设计
  4. 04代码分析特性使用指南
  5. 05公共 API 兼容性检查

适用场景

  1. 01在旧代码库中启用 NRT
  2. 02检查可空警告与空值防护
  3. 03为库 API 标注空值行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill csharp-nullable-reference-types

安装后在 Claude Code 中输入/csharp-nullable-reference-types确认已加载。

附带文件nrt-migration-playbook-reference.mdnullable-attributes-reference.md

详细介绍

C# 可空引用类型

适用场景

  • 在尚未采用可空引用类型(NRT)的代码库中引入 NRT
  • 编写或重构使用 T? / 可空性注解的 C# 代码
  • 使用 System.Diagnostics.CodeAnalysis 可空性特性注解 API
  • 设计需要明确可空性契约的公共/内部 API
  • 包装未注解或旧版 API,让下游调用方仍能受益于 NRT
  • 检查代码中的空状态分析、保护辅助方法和 field 关键字是否正确

核心目标

  • 通过在签名中明确表达空值意图,避免运行时出现 NullReferenceException。
  • 使用官方可空性特性表达类型系统无法直接表示的契约。
  • 在旧版代码库中逐步采用 NRT,无需一次性重写。

核心可空性模型

非可空与可空
  • string — 非可空引用。编译器假定实例永远不为 null;赋值 null 或可能为 null 的值会产生警告。
  • string? — 可空引用。变量可能为 null;编译器要求在解引用前进行 null 检查。
string name = "Alice";
name = null;          // Warning: assigning null to non-nullable.

string? nickname = null;
Console.WriteLine(nickname.Length); // Warning: possible null dereference.
空状态分析(流分析)

编译器会跟踪引用是确定非空还是可能为 null。null 检查和赋值会更新此状态。

string? message = GetMessageOrNull();

if (message != null)
{
    // message is definitely non-null in this block.
    Console.WriteLine(message.Length);
}

// Outside the if, message is maybe null again.

在解引用可空值之前,先进行显式 null 检查(if (x != null)、is not null、模式匹配)。尽早收窄可空性,并保持非空状态。**null 条件赋值(C# 14)**允许你编写 customer?.Order = CreateOrder(); — 仅当接收者非空时才会计算右侧表达式。

null 抑制运算符(!)

x! 告诉编译器“在此处将 x 视为非空”。它只影响分析,不影响运行时行为。

  • 仅当实际不变量能保证非空,而编译器无法识别时,才使用 !。
  • 不要将 ! 作为通用的警告修复方法。优先重构控制流、添加特性或正确初始化成员。
_customer = LoadCustomerFromOrm()!; // ORM guarantees this is not null in valid state.
抑制警告前先重组代码

成功的保护子句或模式匹配已在当前作用域中创建了一个空安全区域。添加 ! 之前,应让可空值只跨过一次经过检查的边界,并保持其余代码为非可空:

  • 使用保护子句或模式匹配尽早收窄可空性;
  • 在检查前将可空字段或属性复制到局部变量,避免重复读取的值在分析过程中发生变化;
  • 如果方法的控制流较复杂,可选择将非空路径移入局部函数或使用非可空参数的私有方法;
  • 将可空值处理保留在边界处,而不是在实现中到处使用 T?、重复检查或 !。
public void Process(Order? order)
{
    if (order?.Customer is not { } customer)
    {
        return;
    }

    // The pattern match already proved that customer is non-null here.
    Console.WriteLine(customer.Name);
}

不要仅为了满足可空性分析而提取函数。当显式的非可空函数边界还能简化大型或包含多个分支的实现时,才采用这种做法。只有当真实的外部不变量无法通过控制流、签名或可空性分析特性表达时,才使用 !。

项目配置

为新代码启用 NRT:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

对于旧版代码库,可通过文件级指令逐步启用(#nullable enable、#nullable disable、#nullable enable warnings、#nullable enable annotations)。应重视 CS86xx 可空性警告;可考虑启用 TreatWarningsAsErrors,或在新项目中将可空性警告视为错误。

请参阅 nrt-migration-playbook-reference.md,了解完整的渐进式采用策略、#nullable 指令参考、旧版互操作,以及已知的静态分析限制(数组、default(struct))。

API 设计规则(签名)

这些规则适用于公共和内部 API 以及模型。

参数 — 如果不允许 null,请使用不可为 null 的类型,并为公共 API 添加运行时保护:

public void SendEmail(string recipient)
{
    ArgumentNullException.ThrowIfNull(recipient);
    // Implementation
}

如果允许 null 且其含义明确,请使用 T?,说明如何解释 null,并实现正确的 null 行为。

返回类型 — 如果方法永远不会返回 null,请使用 Customer;如果方法可能会合理地返回 null,请使用 Customer?(调用方必须检查,编译器也会强制执行)。

public Customer GetRequiredCustomer(Guid id);  // Throws on failure.
public Customer? TryGetCustomer(Guid id);       // Returns null on failure.

属性和字段 — 遵循与参数和返回类型相同的规则。不可为 null 的成员必须通过构造函数、带对象初始化器的 required 属性、由 field 支持的延迟初始化属性,或带 [MemberNotNull] 注解的辅助方法进行初始化。

public class Order
{
    public required string Id { get; init; }
    public required Customer Customer { get; init; }
    public string? Comment { get; init; } // Optional.
}

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

库的公共 API 兼容性

将可空注解和可空性分析属性视为已发布 API 契约的一部分。T 和 T? 具有相同的 CLR 类型,因此,仅更改注解通常与二进制兼容,但可能会破坏源代码兼容性,因为这会让启用了可空性分析的使用方遇到警告。如果使用方将警告视为错误,这些警告通常会导致构建失败。

发布前请审查公共可空性变更,尤其是:

  • 将输出从 T 放宽为 T?,或添加 [MaybeNull];
  • 将输入从 T? 收紧为 T,或添加 [DisallowNull];
  • 更改 class?、class 或 notnull 等泛型约束;
  • 更改虚成员、接口、委托及其实现上的注解或属性,因为不匹配会产生编译器警告。

为先前未启用可空性分析的 API 添加注解,也可能造成同样的源代码兼容性问题。请将添加注解后的 API 表面与上一个已发布版本进行比较,测试启用了可空性分析的使用方,并根据库的兼容性政策记录有意进行的源代码破坏性变更,或为其设定版本。

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

field 上下文关键字让你无需声明显式的支持字段,即可编写属性访问器主体。这是 NRT 的主要使用场景之一(延迟初始化属性),编译器会执行特殊的空值容错分析,因此不会在构造函数中出现烦人的 CS8618:

public class C
{
    public C() { } // No warning: the getter is null-resilient.
    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 关键字可空性规则。每项均包含用途、模式和 Agent 规则。
  • nrt-migration-playbook-reference.md:渐进式采用策略、#nullable 指令参考、旧版/未添加注解的 API 互操作、为较旧目标框架填充可空性属性(包含权衡及添加前确认流程)、已知的静态分析限制(不可为 null 的引用类型数组、带引用字段的 default(struct))、警告处理,以及完整的生成检查清单。

生成检查清单(摘要)

  1. 项目 — 存在 <Nullable>enable</Nullable>;仅在不可避免的旧代码周围禁用。
  2. 类型 — 必需的参数、返回值和属性使用非 nullable 类型;仅当 null 有效且符合预期时才使用 T?。
  3. 初始化 — 使用构造函数、required + 对象初始值设定项、由 field 支持的延迟 getter,或 [MemberNotNull] 辅助方法。除非作为有文档说明的逃生口,否则避免使用 null!。
  4. 空值检查 — 在公共边界显式进行保护;通过控制流缩小范围,仅在能简化复杂代码时才提取非 nullable 辅助方法;仅在有明确不变量时使用 !。
  5. 特性 — 使用特性表达类型系统无法表达的契约(参见参考文件)。
  6. 互操作 — 信任 BCL/带注释的库;包装未标注的 API 时,添加自己的保护和特性。
  7. 警告 — 绝不忽略;修正设计或添加特性,而不是通过 ! 或 #pragma 禁用警告。
  8. 兼容性 — 对于已发布的库,将公共 nullable 性更改视为可能造成源代码破坏的更改,并测试启用了 nullable 的使用方。

参考资料

评论

支持 Markdown