解决可以为 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
}
在上例中,警告是因为 Container
和 c
的 States
属性可能具有 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; }
}
FirstName
和 LastName
均不保证初始化。 如果此代码为新代码,请考虑更改公共接口。 可以按以下方式更新上面的示例:
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.MemberNotNullAttribute 和 System.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 的字符串。 如果 string
和 string?
反转,这是允许的情况,因为派生的类更严格。 同样,参数声明应匹配。 重写方法中的参数可允许 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 以及其他任意值。