csharp-nullable-reference-types

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

C# Nullable Reference Types

C# 可空引用类型

When to Use

何时使用

  • Introducing nullable reference types (NRT) into a codebase that has not yet adopted them
  • Writing or refactoring C# code that uses
    T?
    / nullable annotations
  • Annotating APIs with
    System.Diagnostics.CodeAnalysis
    nullable attributes
  • Designing public/internal APIs where nullability contracts matter
  • Wrapping unannotated or legacy APIs so downstream callers still benefit from NRT
  • Reviewing code for correct null-state analysis, guard helpers, and the
    field
    keyword
  • 向尚未采用可空引用类型(NRT)的代码库引入 NRT
  • 编写或重构使用
    T?
    / 可空注解的 C# 代码
  • 使用
    System.Diagnostics.CodeAnalysis
    可空性属性为 API 添加注解
  • 设计对可空性契约有要求的 public/internal API 和模型
  • 包装未添加注解或旧版 API,使下游调用方仍能受益于 NRT
  • 审查代码中的空值状态分析、保护条件辅助方法和
    field
    关键字是否正确

Core Goals

核心目标

  • Prevent
    NullReferenceException
    at runtime by making null intent explicit in signatures.
  • Express contracts the type system cannot represent directly using the official nullable attributes.
  • Adopt NRT incrementally in legacy codebases without a big-bang rewrite.
  • 通过在签名中明确 null 意图,防止运行时出现
    NullReferenceException
    。
  • 使用官方可空性属性表达类型系统无法直接表示的契约。
  • 在不进行一次性大规模重写的情况下,逐步在旧代码库中采用 NRT。

Core Nullability Model

核心可空性模型

Non-nullable vs nullable

不可空与可空

  • string
    — non-nullable reference. The compiler assumes instances are never
    null
    ; assigning
    null
    or a maybe-null value produces a warning.
  • string?
    — nullable reference. The variable may be
    null
    ; the compiler requires a null check before dereference.
csharp
string name = "Alice";
name = null;          // Warning: assigning null to non-nullable.

string? nickname = null;
Console.WriteLine(nickname.Length); // Warning: possible null dereference.
  • string
    — 不可空引用类型。编译器假定实例永不为
    null
    ;赋值
    null
    或可能为 null 的值会产生警告。
  • string?
    — 可空引用类型。变量可能为
    null
    ;解引用前,编译器要求进行 null 检查。
csharp
string name = "Alice";
name = null;          // Warning: assigning null to non-nullable.

string? nickname = null;
Console.WriteLine(nickname.Length); // Warning: possible null dereference.

Null-state analysis (flow)

空值状态分析(流分析)

The compiler tracks whether a reference is definitely non-null or maybe null. Null checks and assignments update this state.
csharp
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.
Introduce explicit null checks (
if (x != null)
,
is not null
, pattern matching) before dereferencing nullable values. Narrow nullability early and keep the non-null state alive. Null-conditional assignment (C# 14) lets you write
customer?.Order = CreateOrder();
— the right side is evaluated only when the receiver is non-null.
编译器跟踪引用是确定为非 null还是可能为 null。Null 检查和赋值会更新此状态。
csharp
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-conditional assignment(C# 14)**允许编写
customer?.Order = CreateOrder();
——仅当接收对象非 null 时才会计算右侧。

Null-forgiving operator (
!
)

Null-forgiving operator(
!
)

x!
tells the compiler "treat
x
as non-null here." It affects analysis only, not runtime behavior.
  • Use
    !
    only when a real invariant guarantees non-null and the compiler cannot see it.
  • Do not use
    !
    as a general fix for warnings. Prefer refactoring control flow, adding attributes, or proper member initialization.
csharp
_customer = LoadCustomerFromOrm()!; // ORM guarantees this is not null in valid state.
x!
告诉编译器“在此处将
x
视为非 null”。它只影响分析,不改变运行时行为。
  • 仅当真实的不变量保证值非 null、但编译器无法识别时,才使用
    !
    。
  • 不要将
    !
    作为解决警告的通用手段。应优先重构控制流、添加属性或正确初始化成员。
csharp
_customer = LoadCustomerFromOrm()!; // ORM guarantees this is not null in valid state.

Reorganize code before suppressing warnings

在抑制警告前重组代码

A successful guard clause or pattern match already creates a null-safe region in the current scope. Before adding
!
, make nullable values cross a checked boundary once and keep the remaining code non-nullable:
  • narrow early with a guard clause or pattern match;
  • copy nullable fields or properties to a local before checking, so repeated reads cannot change underneath the analysis;
  • when a method has complex control flow, optionally move the non-null path into a local function or private method with non-nullable parameters;
  • keep nullable handling at the boundary instead of spreading
    T?
    , repeated checks, or
    !
    through the implementation.
csharp
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);
}
Do not extract a function solely to satisfy nullable analysis. Use an explicit non-nullable function boundary when it also simplifies a large or branching implementation. Use
!
only when a real external invariant cannot be represented through control flow, signatures, or nullable-analysis attributes.
成功的 guard clause 或模式匹配会在当前作用域中创建 null-safe 区域。在添加
!
前,应让可空值仅跨越一次已检查边界,并使剩余代码保持不可空:
  • 使用 guard clause 或模式匹配尽早收窄可空性;
  • 检查前先将可空字段或属性复制到局部变量,防止重复读取在分析期间发生变化;
  • 当方法具有复杂控制流时,可选择将非空路径移入采用不可空参数的局部函数或私有方法;
  • 将可空处理保留在边界,不要让
    T?
    、重复检查或
    !
    扩散到实现内部。
csharp
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);
}
不要仅仅为了满足可空性分析而提取函数。仅当显式的不可空函数边界也能简化大型或分支较多的实现时,才使用这种方式。只有当真实的外部不变量无法通过控制流、签名或可空性分析属性表示时,才使用
!
。

Project Configuration

项目配置

Enable NRT for new code:
xml
<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>
For legacy codebases, enable incrementally with file-level directives (
#nullable enable
,
#nullable disable
,
#nullable enable warnings
,
#nullable enable annotations
). Treat
CS86xx
nullable warnings as important; consider
TreatWarningsAsErrors
or treating nullable warnings as errors in new projects.
See nrt-migration-playbook-reference.md for the full incremental-adoption strategy,
#nullable
directive reference, legacy interop, and known static-analysis limitations (arrays,
default(struct)
).
为新代码启用 NRT:
xml
<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 Design Rules (Signatures)

API 设计规则(签名)

These rules apply to public and internal APIs and to models.
Parameters — if
null
is not allowed, use a non-nullable type and add a runtime guard for public APIs:
csharp
public void SendEmail(string recipient)
{
    ArgumentNullException.ThrowIfNull(recipient);
    // Implementation
}
If
null
is allowed and meaningful, use
T?
, document how
null
is interpreted, and implement correct
null
behavior.
Return types —
Customer
when the method never returns
null
;
Customer?
when it can legitimately return
null
(callers must check, and the compiler enforces it).
csharp
public Customer GetRequiredCustomer(Guid id);  // Throws on failure.
public Customer? TryGetCustomer(Guid id);       // Returns null on failure.
Properties and fields — follow the same rules as parameters and return types. Non-nullable members must be initialized in constructors, via
required
properties with object initializers, via
field
-backed lazy properties, or via helpers annotated with
[MemberNotNull]
.
csharp
public class Order
{
    public required string Id { get; init; }
    public required Customer Customer { get; init; }
    public string? Comment { get; init; } // Optional.
}
When contracts depend on input/output behavior, conditional behavior, or member initialization, apply the nullable attributes described in nullable-attributes-reference.md.
这些规则适用于 public 和 internal API 以及模型。
参数——如果不允许
null
,请使用不可空类型,并为 public API 添加运行时保护:
csharp
public void SendEmail(string recipient)
{
    ArgumentNullException.ThrowIfNull(recipient);
    // Implementation
}
如果允许
null
且具有实际意义,请使用
T?
,说明如何解释
null
,并实现正确的
null
行为。
返回类型——方法永不返回
null
时使用
Customer
;可以合法返回
null
时使用
Customer?
(调用方必须检查,编译器会强制执行)。
csharp
public Customer GetRequiredCustomer(Guid id);  // Throws on failure.
public Customer? TryGetCustomer(Guid id);       // Returns null on failure.
属性和字段——遵循与参数和返回类型相同的规则。不可空成员必须通过构造函数初始化、通过带对象初始化器的
required
属性初始化、通过基于
field
的惰性属性初始化,或通过带
[MemberNotNull]
注解的辅助方法初始化。
csharp
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 中描述的可空性属性。

Public API compatibility for libraries

库的 public API 兼容性

Treat nullable annotations and nullable-analysis attributes as part of a shipped API contract.
T
and
T?
have the same CLR type, so annotation-only changes are generally binary compatible, but they can be source breaking by introducing warnings for nullable-enabled consumers. Those warnings often become build failures when consumers treat warnings as errors.
Review public nullability changes before release, especially:
  • weakening an output from
    T
    to
    T?
    or adding
    [MaybeNull]
    ;
  • tightening an input from
    T?
    to
    T
    or adding
    [DisallowNull]
    ;
  • changing generic constraints such as
    class?
    ,
    class
    , or
    notnull
    ;
  • changing annotations or attributes on virtual members, interfaces, delegates, and implementations, where mismatches produce compiler warnings.
Adding annotations to a previously nullable-oblivious API can create the same source-compatibility problems. Compare the annotated surface with the last released version, test a nullable-enabled consumer, and document or version intentional source-breaking changes according to the library's compatibility policy.
将可空性注解和可空性分析属性视为已发布 API 契约的一部分。
T
和
T?
具有相同的 CLR 类型,因此仅涉及注解的更改通常保持二进制兼容性,但可能通过为已启用可空性的消费者引入警告而造成源兼容性破坏。当消费者将警告视为错误时,这些警告通常会变成构建失败。
发布前应审查 public 可空性更改,尤其是:
  • 将输出从
    T
    弱化为
    T?
    ,或添加
    [MaybeNull]
    ;
  • 将输入从
    T?
    收紧为
    T
    ,或添加
    [DisallowNull]
    ;
  • 更改泛型约束,例如
    class?
    、
    class
    或
    notnull
    ;
  • 更改虚拟成员、接口、委托及其实现上的注解或属性,因为不匹配会产生编译器警告。
为以前属于 nullable-oblivious 的 API 添加注解,也可能产生相同的源兼容性问题。将添加注解后的 API 表面与上一个发布版本进行比较,测试已启用可空性的消费者,并按照库的兼容性策略记录或版本化有意造成的源兼容性破坏。

The
field
Keyword (C# 14 / .NET 10)

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

The
field
contextual keyword lets you write a property accessor body without declaring an explicit backing field. This is a primary NRT scenario (lazily-initialized properties) and the compiler performs a special null-resilience analysis so you do not get nuisance
CS8618
in constructors:
csharp
public class C
{
    public C() { } // No warning: the getter is null-resilient.
    string Prop => field ??= GetPropValue();
}
See nullable-attributes-reference.md for the full
field
nullability rules (null-resilient vs non-resilient getters, the
[field: AllowNull, MaybeNull]
escape hatch, and setter/constructor analysis).
field
上下文关键字允许在声明显式后备字段的情况下编写属性访问器主体。这是主要的 NRT 应用场景(惰性初始化属性),编译器会执行特殊的 null-resilience 分析,因此构造函数中不会出现多余的
CS8618
警告:
csharp
public class C
{
    public C() { } // No warning: the getter is null-resilient.
    string Prop => field ??= GetPropValue();
}
有关完整的
field
可空性规则(null-resilient 与 non-resilient getter、
[field: AllowNull, MaybeNull]
escape hatch,以及 setter/构造函数分析),请参阅 nullable-attributes-reference.md。

Reference Files

参考文件

  • nullable-attributes-reference.md: The complete
    System.Diagnostics.CodeAnalysis
    attribute catalog — preconditions (
    AllowNull
    ,
    DisallowNull
    ), postconditions (
    MaybeNull
    ,
    NotNull
    ), conditional postconditions (
    NotNullWhen
    ,
    MaybeNullWhen
    ,
    NotNullIfNotNull
    ), helper methods (
    MemberNotNull
    ,
    MemberNotNullWhen
    ), unreachable-code helpers (
    DoesNotReturn
    ,
    DoesNotReturnIf
    ), and the
    field
    keyword nullability rules. Each with intent, pattern, and agent rules.
  • nrt-migration-playbook-reference.md: Incremental adoption strategy,
    #nullable
    directive reference, legacy/unannotated API interop, polyfilling nullable attributes for older target frameworks (with tradeoffs and a confirm-before-adding decision process), known static-analysis limitations (arrays of non-nullable references,
    default(struct)
    with reference fields), warning handling, and the full generation checklist.
  • 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 互操作、为旧目标框架 polyfill 可空性属性(涉及权衡和添加前确认决策流程)、已知静态分析限制(非 null 引用数组、包含引用字段的
    default(struct)
    )、警告处理,以及完整的生成检查清单。

Generation Checklist (Summary)

生成检查清单(摘要)

  1. Project —
    <Nullable>enable</Nullable>
    present; disable only around unavoidable legacy code.
  2. Types — non-nullable for required params/returns/properties;
    T?
    only when
    null
    is valid and expected.
  3. Initialization — constructors,
    required
    + object initializers,
    field
    -backed lazy getters, or
    [MemberNotNull]
    helpers. Avoid
    null!
    except as a documented escape hatch.
  4. Null checks — explicit guards at public boundaries; narrow with control flow, and extract a non-nullable helper only when it improves complex code;
    !
    only with a clear invariant.
  5. Attributes — apply to express contracts the type system cannot express (see reference file).
  6. Interop — trust BCL/annotated libraries; add your own guards and attributes when wrapping unannotated APIs.
  7. Warnings — never ignore; fix design or add attributes rather than suppressing with
    !
    or
    #pragma
    .
  8. Compatibility — for released libraries, review public nullability changes as potential source breaks and test nullable-enabled consumers.
  1. 项目——存在
    <Nullable>enable</Nullable>
    ;仅在不可避免的旧代码周围禁用。
  2. 类型——必需参数/返回值/属性使用不可空类型;仅当
    null
    有效且符合预期时使用
    T?
    。
  3. 初始化——使用构造函数、
    required
    + 对象初始化器、基于
    field
    的惰性 getter,或
    [MemberNotNull]
    辅助方法。除非作为有文档说明的 escape hatch,否则避免使用
    null!
    。
  4. Null 检查——在 public 边界设置显式 guard;通过控制流收窄可空性,仅当有助于简化复杂代码时才提取不可空 helper;仅在具有明确不变量时使用
    !
    。
  5. 属性——应用属性来表达类型系统无法表达的契约(参阅参考文件)。
  6. 互操作——信任 BCL/已添加注解的库;包装未添加注解的 API 时,添加自己的 guard 和属性。
  7. 警告——绝不忽略警告;应修复设计或添加属性,而不是使用
    !
    或
    #pragma
    进行抑制。
  8. 兼容性——对于已发布的库,应将 public 可空性更改视为潜在的源兼容性破坏进行审查,并测试已启用可空性的消费者。

References

参考资料