解决可以为 null 的警告

Tip

不熟悉可为 null 的引用类型? 请先阅读 可为空的引用类型,以了解注释和 null 状态分析。 本文假定你在启用了该功能的项目中看到警告。

正在查找特定的编译器错误代码? 解决可为 null 警告参考文章列出了每个 CS86xx 警告及其对应的处理方法。

启用可为 null 的引用类型后,凡是代码的行为与其注解不一致的地方,编译器都会发出警告。 大多数警告都可归为少数几种模式。 识别模式后,修复通常是五种技术之一:

  • 添加空值检查。
  • 添加或删除一个?!注释。
  • 添加描述 null 协定的属性。
  • 正确初始化变量。
  • 验证项目设置。

本文通过一个具有代表性的示例演练每个技术。 目标不是消除警告。 它使代码的 null 处理意向明确,以便编译器得出相同的结论。

空性状态:编译器跟踪的内容

在了解这些技术之前,先了解编译器如何跟踪潜在的 null 状态冲突问题会有所帮助。 在读取你的代码时,编译器会跟踪每个表达式的 null 状态:即它对该表达式在代码中的该位置是否可能为 null 的分析。 null 状态是两个值之一:

  • not-null - 编译器可以证明表达式不在 null 此处。 无需检查即可安全地使用它。
  • maybe-null - 编译器无法排除 null。 在不检查表达式的情况下使用表达式会生成警告。

当编译器遵循代码时,变量的 null 状态更改。 可能返回 null 的方法会生成 可能为 null 的结果。 对 if (x is not null) 的检查会将 xif 代码块内缩小为 非 null。 你看到的警告是编译器告诉你,它确定表达式处于 可能为 null 状态,你即将使用它,就好像它 不是 null 一样。 本文其余部分的每个技术都是为编译器提供在使用它之前确保表达式 不为 null 所需的信息的不同方法。

添加 null 值检查

最常见的警告是 可能解引用 null。 编译器将变量的 null 状态跟踪为 可能为 null ,并看到未检查使用的变量:

public static int LengthOfMessageUnsafe(string? message)
{
    // Warning CS8602: dereference of a possibly null reference.
    return message.Length;
}

通常的解决办法是保护子句保护子句是位于方法或代码块开头的一种检查,用于在输入无效时返回或抛出异常。 只有安全路径会继续延伸。 运行检查后,编译器会将变量的 null 状态更新为安全路径上的 not-null

public static int DereferenceFixed(string? message)
{
    if (message is null)
    {
        return 0;
    }

    // No warning: the compiler knows message is not-null on this path.
    return message.Length;
}

模式匹配(用于测试值形状的表达式,例如 is nullis { })、????= 都包含 null 检查:

public static int NullOperatorsFix(string? message)
{
    // ?. evaluates to null if message is null; ?? supplies the fallback value.
    int length = message?.Length ?? 0;

    // Pattern matching narrows the type on the matching branch.
    if (message is { Length: > 0 })
    {
        length = message.Length;
    }

    return length;
}

仅当 message 为非 null Length 属性大于零时,{ Length: > 0 } 属性模式才会匹配,因此编译器会在 if 块内将 message 视为 非 null。 更简单的 is not null 测试无需检查任何属性,也能实现相同的 null 状态缩小。

要深入了解这些运算符,请参阅 空值运算符

调整批注

当代码将 可能为 null 表达式分配给不可为 null 的变量时,编译器也会发出警告。 该警告意味着以下两项之一:

  • 变量应允许 null 值。 在这种情况下,请向类型添加一个 ?
  • 表达式永远不会生成 null 值。 标注生成该内容的 API。
public static void AssignmentWarning()
{
    // Warning CS8600: converting null literal or possible null value to non-nullable type.
    string name = Lookup("nobody");
    Console.WriteLine(name);
}

如果 Lookup 合法返回 null,请更改调用站点以接受缺失值:

public static void AssignmentFixed()
{
    string? name = Lookup("somebody");
    if (name is not null)
    {
        Console.WriteLine(name);
    }
}

如果 Lookup 从未返回 null,请更改其签名以返回不可为 null 的引用类型。 返回的值的 null 状态取决于输入的情况,请参阅以下有关 null 分析属性的部分。

仅当可以保证值不为 null 但不能在类型系统中表示该保证时,才使用 null 放弃运算符 ! 。 每个 ! 位置都是编译器无法再保护你的地方,因此首选添加检查或批注源 API。

添加空值分析属性

有时正确的修复不在呼叫站点。 方法的签名无法准确捕获其输入和输出之间的关系,编译器在其他安全代码中发出警告:

public static bool IsPresent(string? text) =>
    !string.IsNullOrEmpty(text);

public static void CallerWithoutAttribute(string? text)
{
    if (IsPresent(text))
    {
        // Warning CS8602: dereference of a possibly null reference.
        // The signature doesn't tell the compiler text is not-null here.
        Console.WriteLine(text.Length);
    }
}

IsPresent 的主体证明了当方法返回 true 时,该参数不为 null,但签名并没有说明这一点。 添加 可空性分析特性,使协定成为 API 的一部分:

public static bool AttributedIsPresent([NotNullWhen(true)] string? text) =>
    !string.IsNullOrEmpty(text);

public static void CallerWithAttribute(string? text)
{
    if (AttributedIsPresent(text))
    {
        // No warning: the attribute tells the compiler text is not-null.
        Console.WriteLine(text.Length);
    }
}

常见属性包括:

完整列表见 可为 null 的静态分析属性

初始化不可为 null 的成员

构造函数警告表示:不可为 null 的字段、属性或 自动属性(即使用编译器生成的后备字段的属性,例如 public string Name { get; set; })在构造函数结束时仍未被赋予非 null 值:

public class PersonUninitialized
{
    // Warning CS8618: Non-nullable property 'Name' is uninitialized.
    public string Name { get; set; }
}

可以通过多种方式解决它。 选择最符合您的设计意图的一项。

需要将值作为构造函数参数。 使用 主构造函数 (在类型本身上声明的参数,在整个正文中可用)或初始化属性的常规构造函数:

public class PersonInjected(string name)
{
    public string Name { get; } = name;
}

将该属性设为 required 调用方必须通过 对象初始值设定项 初始化它( { Property = value } 下面的 new语法):

public class PersonRequired
{
    public required string Name { get; init; }
}

使用默认值进行初始化。 当类型具有有意义的空值时,请在声明中初始化:

public class PersonInitialized
{
    public string Name { get; set; } = "John Doe";
}

Tip

仅当该类型确实具有良好的默认值时,才选择这种技术:也就是说,该默认值必须是一个有效且功能完备、可供调用方直接使用的实例。 示例包括空集合。 不要发明哨兵值(即像String.Empty"N/A""unknown"-1这样的占位值,并将其视为“无值”)来充当null的替代:这样虽能屏蔽警告,但每个调用方都必须知道并检查这个哨兵值,而类型系统对此也无能为力。 如果没有合适的默认值,则应将该属性设为可空。

使属性可为 null。 如果该值确实可能缺失,请将类型改为可为 null:

public class PersonOptional
{
    public string? Name { get; set; }
}

如果某个辅助方法会初始化该成员,请使用 MemberNotNullAttribute 对该辅助方法进行注解,以便编译器将对它的调用视为该成员已初始化。

验证项目设置

默认情况下,新建的 C# 项目会默认启用可为 null 的引用类型功能,因此你编写或阅读的大多数代码都已经启用了该功能。 通常不需要配置任何内容。 如果你想了解某个项目是否启用了该功能,或者需要更改此设置,请在 .csproj 中查找 <Nullable> 元素:

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

常见的支持值是 enable (新项目的默认值)和 disable。 如果元素缺失,项目将使用 SDK 和目标框架集的默认任何内容。

如果需要仅在文件的部分内容中使用 #nullable 指令启用 nullable,或者在迁移现有代码库时使用 warningsannotations 部分模式,请参阅 nullable 迁移策略

下一步要访问的位置

当警告不适合上述任何模式时, 解析可为 null 的警告 参考文章列出了编译器发出的每个 CS86xx 警告的技术。

若要规划如何在现有代码库中逐步启用可空引用类型,请参阅 可空引用类型迁移策略