解决可以为 null 的警告

本文介绍以下编译器警告:

  • CS8597 - 抛出的值可能为 null。
  • CS8600 - 将 null 字面量或可能为 null 的值转换为非 null 类型。
  • CS8601 - 引用类型赋值可能为空。
  • CS8602 - 解引用可能为 null 的引用。
  • CS8603 - 可能返回空引用。
  • CS8604 - 形参可能传入 null 引用实参。
  • CS8605 - 取消装箱可能为 null 的值。
  • CS8607 - 可能的 null 值不能用于标有 [NotNull][DisallowNull] 的类型
  • CS8608 - 类型中引用类型的为 Null 性与重写成员不匹配。
  • CS8609 - 返回类型中引用类型的为 Null 性与重写成员不匹配。
  • CS8610 - 参数类型中引用类型的为 Null 性与重写成员不匹配。
  • CS8611 - 参数类型中引用类型的为 Null 性与分部方法声明不匹配。
  • CS8612 - 类型中引用类型的为 Null 性与隐式实现的成员不匹配。
  • CS8613 - 返回类型中引用类型的为 Null 性与隐式实现的成员不匹配。
  • CS8614 - 参数类型中引用类型的为 Null 性与隐式实现的成员不匹配。
  • CS8615 - 类型中引用类型的为 Null 性与实现的成员不匹配。
  • CS8616 - 返回类型中引用类型的为 Null 性与实现的成员不匹配。
  • CS8617 - 参数类型中引用类型的为 Null 性与实现的成员不匹配。
  • CS8618 - 在退出构造函数时,不可为 null 的变量必须包含非 null 值。请考虑声明为可以为 null。
  • CS8619 - 值中的引用类型的为 Null 性与目标类型不匹配。
  • CS8620 - 由于引用类型的可为 null 性差异,实参不能用于形参。
  • CS8621 - 返回类型中引用类型的为 Null 性与目标委托不匹配(可能是由于为 Null 性特性)。
  • CS8622 - 参数类型中引用类型的为 Null 性与目标委托不匹配(可能是由于为 Null 性特性)。
  • CS8624 - 由于引用类型的可为 null 性差异,实参不能用作输出。
  • CS8625 - 无法将 null 字面量转换为非 null 的引用类型。
  • CS8629 - 可为 null 的值类型可为 null。
  • CS8631 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与约束类型不匹配。
  • CS8633 - 方法的类型参数的约束中的为 Null 性与接口方法的类型参数的约束不匹配。请考虑改用显式接口实现。
  • CS8634 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与“class”约束不匹配。
  • CS8643 - 显式接口说明符中引用类型的 Null 性与该类型实现的接口不匹配。
  • CS8644 - 类型不实现接口成员。接口中基类型实现的引用类型的 Null 性不匹配。
  • CS8645 - 成员已列入类型的接口列表中,其中包含不同引用类型的 Null 性。
  • CS8655 - Switch 表达式不会处理某些为 null 的输入(它并非详尽无遗)。
  • CS8667 - 分部方法声明在对类型参数的约束中具有不一致的为 Null 性。
  • CS8670 - 对象或集合初始值设定项会隐式解引用可能为 null 的成员。
  • CS8714 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与“notnull”约束不匹配。
  • CS8762 - 退出时,参数必须具有非 null 值。
  • CS8763 - 不能返回标记为 [DoesNotReturn] 的方法。
  • CS8764 - 返回类型的为 Null 性与重写成员不匹配(可能是由于为 Null 性特性)。
  • CS8765 - 参数类型的为 Null 性与重写成员不匹配(可能是由于为 Null 性特性)。
  • CS8766 - 返回类型中引用类型的为 Null 性与隐式实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8767 - 参数类型中引用类型的为 Null 性与隐式实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8768 - 返回类型中引用类型的为 Null 性与实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8769 - 参数类型中引用类型的为 Null 性与实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8770 - 方法缺少 [DoesNotReturn] 注释,无法匹配已实现的或被替代的成员。
  • CS8774 - 退出时,成员必须具有非 null 值。
  • CS8776 - 不能在此特性中使用此成员。
  • CS8775 - 退出时,成员必须具有非 null 值。
  • CS8777 - 退出时,参数必须具有非 null 值。
  • CS8819 - 返回类型中引用类型的为 Null 性与分部方法声明不匹配。
  • CS8824 - 退出时参数必须具有非 null 值,因为参数是非 null。
  • CS8825 - 由于参数为非 null,因此返回值必须为非 null。
  • CS8847 - Switch 表达式不会处理一些 null 输入(它不是穷举)。但是,带有“when”子句的模式可能成功匹配此值。

可以为 null 的警告的作用是将应用程序在运行时引发 System.NullReferenceException 的机率降到最低。 若要实现此目标,编译器将使用静态分析,并在代码具有可能导致空引用异常的构造时发出警告。 通过应用类型注释和属性为编译器提供其静态分析的信息。 这些注释和特性描述了自变量、参数和类型成员的可为 null 性。 在本文中,你将学习不同的技术来处理编译器从其静态分析生成的可为 null 的警告。 此处所述的方法适用于一般 C# 代码。 通过阅读使用可为 null 的引用类型,了解如何使用可以为 null 的引用类型和实体框架核心。

使用以下四种方法之一可处理几乎所有警告:

  • 添加必要的 null 检查。
  • 添加 ?! 可为 null 的注释。
  • 添加描述 null 语义的特性。
  • 正确初始化变量。

可能的取消 null 引用

这一组警告会提醒你正在对一个“null 状态”是“可能为 null”的变量执行取消引用操作。 这些警告如下:

  • CS8602 - 解引用可能为 null 的引用。
  • CS8670 - 对象或集合初始值设定项会隐式解引用可能为 null 的成员。

以下代码演示上述每个警告的一个示例:

class Container
{
    public List<string>? States { get; set; }
}

internal void PossibleDereferenceNullExamples(string? message)
{
    Console.WriteLine(message.Length); // CS8602

    var c = new Container { States = { "Red", "Yellow", "Green" } }; // CS8670
}

在上例中,警告是因为 ContainercStates 属性可能具有 null 值。 向可能为 null 的集合分配新状态会导致警告。

若要删除这些警告,需要在取消引用之前添加代码,将该变量的“null 状态”更改为“不为 null”。 集合初始值设定项警告可能更难以发现。 初始化表达式向集合添加元素时,编译器检测到该集合可能为 null。

在许多情况下,可以通过在对变量进行取消引用之前检查变量是否为 null 来消除这些警告。 请考虑以下示例,它在取消引用 message 参数之前添加 null 检查:

void WriteMessageLength(string? message)
{
    if (message is not null)
    {
        Console.WriteLine(message.Length);
    }
    
}

以下示例初始化 States 的后备存储并移除 set 访问器。 类的使用者可以修改集合的内容,并且集合的存储永远不会为 null

class Container
{
    public List<string> States { get; } = new();
}

其他情况如果收到这些警告,则可能为误报。 你可能有一个专用的实用程序方法,用于测试 null 状态。 编译器不知道此方法提供了 null 检查。 请看下面的示例,该示例使用专用实用程序方法 IsNotNull

public void WriteMessage(string? message)
{
    if (IsNotNull(message))
        Console.WriteLine(message.Length);
}

编译器会警告你在编写属性 message.Length 时可能会取消 null 引用,因为其静态分析确定 message 可能是 null。 你可能知道 IsNotNull 提供了一个 null 检查,当它返回 true 时,message 的 null 状态应为“不为 null”。 需要告知编译器这些事实。 一种方法是使用 null 包容性运算符 !。 可以更改 WriteLine 语句,使其与以下代码相匹配:

Console.WriteLine(message!.Length);

Null 包容性运算符使表达式“不为 null”,即使它曾经“可能为 null”且未应用 !。 在此示例中,更好的解决方案是将一个特性添加到 IsNotNull 的签名:

private static bool IsNotNull([NotNullWhen(true)] object? obj) => obj != null;

当方法返回 true 时,System.Diagnostics.CodeAnalysis.NotNullWhenAttribute 通知编译器为 obj 参数使用的参数“不为 null”。 当该方法返回 false 时,参数具有在调用方法之前其曾经具有的 null 状态。

提示

有一组丰富的属性可用于描述方法和属性如何影响 null 状态。 可以在语言参考文章可为 null 的静态分析特性中了解这些属性。

如果要消除一个指示正在取消引用一个可能为 null 的变量的警告,有以下三种方法之一:

  • 添加缺少的 null 检查。
  • 在 API 上添加 null 分析特性,以影响编译器的 null 状态 静态分析。 这些属性会通知编译器调用方法后何时应返回“可能为 null”或“不为 null”的值或参数为。
  • 将 null 包容性运算符 ! 应用到表达式,强制性使状态成为“不为 null”。

向不可为 null 的引用分配了可能为 null 的引用

这一组警告会提醒你正在将一个类型为不可为 null 的变量分配给一个 null 状态为可能为 null 的表达式。 这些警告如下:

  • CS8597 - 抛出的值可能为 null。
  • CS8600 - 将 null 字面量或可能为 null 的值转换为非 null 类型。
  • CS8601 - 引用类型赋值可能为 null。
  • CS8603 - 可能返回 null 引用。
  • CS8604 - 形参可能传入 null 引用实参。
  • CS8605 - 取消装箱可能为 null 的值。
  • CS8625 - 无法将 null 字面量转换为非 null 的引用类型。
  • CS8629 - 可为 null 的值类型可为 null。

当你尝试将可能为 null 的表达式分配给不可 null 的变量时,编译器将发出这些警告。 例如:

string? TryGetMessage(int id) => "";

string msg = TryGetMessage(42);  // Possible null assignment.

各种警告指示提供有关代码的详细信息,例如赋值、取消装箱赋值、返回语句、方法参数和引发表达式。

可以执行三种操作来消除这些警告。 一种是添加 ? 注释,使变量成为可以为 null 的引用类型。 此更改可能会导致其他警告。 将变量从不可为 null 的引用更改为可为 null 的引用会将其默认的 null 状态从“不为 null”更改为“可能为 null”。 编译器的静态分析可能会找到一些实例,在这些实例中,会取消引用可能为 null 的变量。

另外两种操作会指示编译器,赋值的右侧不为 null。 赋值前,右侧的表达式可为 null,如以下示例中所示:

string notNullMsg = TryGetMessage(42) ?? "Unknown message id: 42";

前面的示例演示如何向方法的返回值赋值。 可以对方法(或属性)进行注释,以指示方法何时返回不为 null 的值。 当输入参数不为 null 时,通常指定不为 null 的返回值System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute。 另一种方法是将 null 包容性运算符 ! 添加到右侧:

string msg = TryGetMessage(42)!;

如果要消除有关将可能为 null 的表达式分配给不为 null 的变量的警告,可以使用以下四种技术之一:

  • 将赋值的左侧更改为可以为 null 的类型。 取消引用该变量时,此操作可能会发出新的警告。
  • 在赋值之前提供 null 检查。
  • 对生成赋值右侧的 API 进行注释。
  • 将 null 包容性运算符添加到赋值的右侧。

不可为 null 的引用未进行初始化

这一组警告会提醒你正在将一个类型为不可为 null 的变量分配给一个 null 状态为可能为 null 的表达式。 这些警告如下:

  • CS8618 - 在退出构造函数时,不可为 null 的变量必须包含非 null 值。请考虑声明为可以为 null。
  • CS8762 - 退出时,参数必须具有非 null 值。

作为一个例子,请查看以下类:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

FirstNameLastName 均不保证初始化。 如果此代码为新代码,请考虑更改公共接口。 可以按以下方式更新上面的示例:

public class Person
{
    public Person(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

如果在设置名称之前需要创建 Person 对象,则可以使用默认的不为 null 的值对属性进行初始化:

public class Person
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
}

另一种方法可能是将这些成员更改为可以为 null 的引用类型。 如果应允许为名称使用 null,则可以按以下方式定义 Person 类:

public class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

现有代码可能需要进行其他更改,以通知编译器这些成员的 null 语义。 你可能已创建多个构造函数,且你的类可能具有用于初始化一个或多个成员的专用帮助器方法。 可以将初始化代码移动到单个构造函数中,并确保所有构造函数都调用具有通用初始化代码的那个构造函数。 或者,可以使用 System.Diagnostics.CodeAnalysis.MemberNotNullAttributeSystem.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute 特性。 这些特性通知编译器,在调用方法后,成员不为 null。 下面的代码就是删除两种空格的示例。 Person 类使用由所有其他构造函数调用的通用构造函数。 Student 类具有使用 System.Diagnostics.CodeAnalysis.MemberNotNullAttribute 特性进行批注的帮助程序方法:


using System.Diagnostics.CodeAnalysis;

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public Person() : this("John", "Doe") { }
}

public class Student : Person
{
    public string Major { get; set; }

    public Student(string firstName, string lastName, string major)
        : base(firstName, lastName)
    {
        SetMajor(major);
    }

    public Student(string firstName, string lastName) :
        base(firstName, lastName)
    {
        SetMajor();
    }

    public Student()
    {
        SetMajor();
    }

    [MemberNotNull(nameof(Major))]
    private void SetMajor(string? major = default)
    {
        Major = major ?? "Undeclared";
    }
}

最后,可以使用 null 包容性运算符指示某个成员在其他代码中初始化。 另一个例子是以下表示 Entity Framework Core 模型的类:

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

public class TodoContext : DbContext
{
    public TodoContext(DbContextOptions<TodoContext> options)
        : base(options)
    {
    }

    public DbSet<TodoItem> TodoItems { get; set; } = null!;
}

DbSet 属性初始化为 null!。 它指示编译器将属性设置成不为 null 的值。 事实上,基 DbContext 执行该集的初始化。 编译器的静态分析不会选取这个操作。 有关使用可为 null 的引用类型和 Entity Framework Core 的详细信息,请参阅在 EF Core 中使用为 null 的引用类型一文。

如果要消除一个指示未初始化不可为 null 的成员的警告,可使用以下四种方法之一:

  • 更改构造函数或字段初始值设定项,以确保所有不可为 null 的成员已初始化。
  • 将一个或多个成员更改为可为 null 的类型。
  • 对任何帮助器方法进行注释以指示分配了哪些成员。
  • 将初始值设定项添加到 null!,以指示该成员在其他代码中初始化。

为 null 性声明中存在不匹配

许多警告指示方法、委托或类型参数的签名之间的为 null 性不匹配。

  • CS8608 - 类型中引用类型的为 Null 性与重写成员不匹配。
  • CS8609 - 返回类型中引用类型的为 Null 性与重写成员不匹配。
  • CS8610 - 参数类型中引用类型的为 Null 性与重写成员不匹配。
  • CS8611 - 参数类型中引用类型的为 Null 性与分部方法声明不匹配。
  • CS8612 - 类型中引用类型的为 Null 性与隐式实现的成员不匹配。
  • CS8613 - 返回类型中引用类型的为 Null 性与隐式实现的成员不匹配。
  • CS8614 - 参数类型中引用类型的为 Null 性与隐式实现的成员不匹配。
  • CS8615 - 类型中引用类型的为 Null 性与实现的成员不匹配。
  • CS8616 - 返回类型中引用类型的为 Null 性与实现的成员不匹配。
  • CS8617 - 参数类型中引用类型的为 Null 性与实现的成员不匹配。
  • CS8619 - 值中的引用类型的为 Null 性与目标类型不匹配。
  • CS8620 - 由于引用类型的可为 null 性差异,实参不能用于形参。
  • CS8621 - 返回类型中引用类型的为 Null 性与目标委托不匹配(可能是由于为 Null 性特性)。
  • CS8622 - 参数类型中引用类型的为 Null 性与目标委托不匹配(可能是由于为 Null 性特性)。
  • CS8624 - 由于引用类型的可为 null 性差异,实参不能用作输出。
  • CS8631 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与约束类型不匹配。
  • CS8633 - 方法的类型参数的约束中的为 Null 性与接口方法的类型参数的约束不匹配。请考虑改用显式接口实现。
  • CS8634 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与“class”约束不匹配。
  • CS8643 - 显式接口说明符中引用类型的 Null 性与该类型实现的接口不匹配。
  • CS8644 - 类型不实现接口成员。接口中基类型实现的引用类型的 Null 性不匹配。
  • CS8645 - 成员已列入类型的接口列表中,其中包含不同引用类型的 Null 性。
  • CS8667 - 分部方法声明在对类型参数的约束中具有不一致的为 Null 性。
  • CS8714 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与“notnull”约束不匹配。
  • CS8764 - 返回类型的为 Null 性与重写成员不匹配(可能是由于为 Null 性特性)。
  • CS8765 - 参数类型的为 Null 性与重写成员不匹配(可能是由于为 Null 性特性)。
  • CS8766 - 返回类型中引用类型的为 Null 性与隐式实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8767 - 参数类型中引用类型的为 Null 性与隐式实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8768 - 返回类型中引用类型的为 Null 性与实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8769 - 参数类型中引用类型的为 Null 性与实现的成员不匹配(可能是由于为 Null 性特性)。
  • CS8819 - 返回类型中引用类型的为 Null 性与分部方法声明不匹配。

以下代码演示了 CS8764:

public class B
{
    public virtual string GetMessage(string id) => string.Empty;
}
public class D : B
{
    public override string? GetMessage(string? id) => default;
}

前面的示例显示了基类中的 virtual 方法和 override 具有不同的为 null 性。 基类返回不可为 null 的字符串,但派生的类返回一个可以为 null 的字符串。 如果 stringstring? 反转,这是允许的情况,因为派生的类更严格。 同样,参数声明应匹配。 重写方法中的参数可允许 null,即使基类不允许。

其他情况下,可能会产生这些警告。 接口方法声明和该方法的实现中可能存在不匹配的情况。 或者委托类型和该委托的表达式可能不同。 类型参数和类型自变量的为 null 性可能不同。

若要消除这些警告,请更新相应的声明。

代码与特性声明不匹配

前面的部分讨论了如何使用可为 null 的静态分析的特性来通知编译器你的代码的 null 语义。 如果代码不遵循该特性的承诺,编译器会发出警告:

  • CS8607 - 可能的 null 值不能用于标有 [NotNull][DisallowNull] 的类型
  • CS8763 - 不能返回标记为 [DoesNotReturn] 的方法。
  • CS8770 - 方法缺少 [DoesNotReturn] 注释,无法匹配已实现的或被替代的成员。
  • CS8774 - 退出时,成员必须具有非 null 值。
  • CS8775 - 退出时,成员必须具有非 null 值。
  • CS8776 - 不能在此特性中使用此成员。
  • CS8777 - 退出时,参数必须具有非 null 值。
  • CS8824 - 退出时参数必须具有非 null 值,因为参数是非 null。
  • CS8825 - 由于参数为非 null,因此返回值必须为非 null。

请考虑以下方法:

public bool TryGetMessage(int id, [NotNullWhen(true)] out string? message)
{
    message = null;
    return true;

}

编译器生成警告,因为 message 参数分配了 null 并且方法返回 true。 特性 NotNullWhen 指示不应发生这种情况。

若要消除这些警告,请更新代码,使其符合有关已应用的特性的预期。 可以更改特性或算法。

详尽的 switch 表达式

switch 表达式必须是详尽的,这意味着必须处理所有输入值。 即使对于不可为 null 的引用类型,也必须考虑 null 值。 未处理 null 值时,编译器会发出警告:

  • CS8655 - Switch 表达式不会处理某些为 null 的输入(它并非详尽无遗)。
  • CS8847 - Switch 表达式不会处理一些 null 输入(它不是穷举)。但是,带有“when”子句的模式可能成功匹配此值。

以下示例代码演示了此条件:

int AsScale(string status) =>
    status switch
    {
        "Red" => 0,
        "Yellow" => 5,
        "Green" => 10,
        { } => -1
    };

输入表达式是 string,而不是 string?。 编译器仍会生成此警告。 { } 模式处理所有非 null 值,但不匹配 null。 若要解决这些错误,你可以添加显式 null 大小写,也可以将 { } 替换为 _(丢弃)模式。 丢弃模式匹配 null 以及其他任意值。