解决可以为 null 的警告 - C# reference

解决可以为 null 的警告 - C# reference

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

可为 null 的引用类型,包括运算符? 和!,仅当可为 null 的上下文设为enable 或annotations时才允许。 可以在项目文件中使用 Nullable编译器选项,或在源代码中使用 #nullable 预处理指令来设置可空上下文。

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

CS8597 - 抛出的值可能为 null。

CS8598 - 在此上下文中不允许使用抑制运算符

CS8600 - 将 null 文本或可能的 null 值转换为非 null 类型。

CS8601 - 引用赋值可能为 null。

CS8602 - 取消引用可能为 null 的引用。

CS8603 - 可能返回 null 引用。

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 性特性)。

CS8623 - 不允许显式应用 System.Runtime.CompilerServices.NullableAttribute 。

CS8624 - 由于引用类型的为 Null 性差异,实参不能用作输出。

CS8625 - 无法将 null 文本转换为非 null 的引用类型。

CS8628 - 无法在对象创建中使用可为 null 的引用类型。

CS8629 - 可为 null 的值类型可为 null。

CS8631 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与约束类型不匹配。

CS8632 - 可为 null 引用类型的注释仅应在 #nullable 注释上下文的代码中使用。

CS8633 - 方法的类型参数的约束中的为 Null 性与接口方法的类型参数的约束不匹配。请考虑改用显式接口实现。

CS8634 - 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与“class”约束不匹配。

CS8636 - 无效选项; /nullable必须是 disable、 enable或 warningsannotations

CS8637 - 预期 enable、 disable或 restore

CS8639 - typeof 运算符不能用于可以为 null 的引用类型

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 的上下文。

添加必要的 null 检查。

添加或删除?或!可为 null 的批注。

添加描述 null 语义的特性。

正确初始化变量。

如果你不熟悉可为 Null 引用类型,可为 Null 引用类型的概述会介绍这些类型能够解决的问题及其工作原理,并说明它们如何为代码中可能出现的错误提供警告。 还可以查看 迁移到可为 null 引用类型的 指南,了解有关在现有项目中启用可为空引用类型的详细信息。

配置可为 null 的上下文

以下警告表明尚未正确设置可为 null 的上下文:

CS8632 - 可为 null 引用类型的注释仅应在 #nullable 注释上下文的代码中使用。

CS8636 - 无效选项; /nullable必须是 disable、 enable或 warningsannotations

CS8637 - 预期 enable、 disable或 restore

若要正确设置可为 null 的上下文,可使用以下两个选项:

项目级配置:将 元素添加到项目文件:

enable

文件级配置:在源代码中使用 #nullable 预处理器指令:

#nullable enable

可为 null 的上下文具有两个用于控制不同方面的独立标志:

批注标志:控制你是否可以使用 ? 来声明可为 null 的引用类型和 ! 来隐藏单个警告。

警告标志:控制编译器是否发出可为 Null 性警告

有关可为 Null 的上下文和迁移策略的详细信息,请参阅:

可为 Null 的引用类型概述

使用可为 null 的引用类型更新代码库

批注语法不正确

这些错误和警告指示使用 ! 或 ? 批注不正确。

CS8598 - 在此上下文中不允许使用抑制运算符

CS8623 - 不允许显式应用 System.Runtime.CompilerServices.NullableAttribute 。

CS8628 - 无法在对象创建中使用可为 null 的引用类型。

CS8639 - typeof 运算符不能用于可以为 null 的引用类型

声明中的 ? 注释指示变量可能为 null。 它不指示不同的运行时类型。 以下两个声明都是相同的运行时类型:

string s1 = "a string";

string? s2 = "another string";

? 是对编译器关于 null 值预期的提示。

表达式 ! 上的批注指示你知道表达式是安全的,应假定该表达式不为 null。

必须使用这些注释,而不是代码中的System.Runtime.CompilerServices.NullableAttribute标签。

由于?是一个注解而不是一个类型,因此不能将其用于typeof或new表达式。

!无法将运算符应用于变量表达式或方法组。

运算符!不能应用于成员访问运算符的左侧,例如obj.Field!.Method()。

可能的取消 null 引用

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

CS8602 - 取消引用可能为 null 的引用。

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

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

class Container

{

public List? 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 States { get; } = new();

}

收到这些警告时的其他实例可能是误报。 你可能具有一个用于测试 null 的专用实用工具方法。 编译器不知道此方法提供了 null 检查。 请看下面的示例,该示例使用专用实用程序方法 IsNotNull:

public void WriteMessage(string? message)

{

if (IsNotNull(message))

Console.WriteLine(message.Length);

}

当你编写属性 message.Length 时,编译器会警告你可能正在取消引用 null,因为其静态分析确定 message 可能为 null。 你知道,IsNotNull 用于进行空值检查,当返回true时,message的空值状态应为非空。 需要告知编译器这些事实。 一种方法是使用 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 forgiving 运算符 ! 应用于强制将状态改为非 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 options)

: base(options)

{

}

public DbSet 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 匹配。

相关推荐

Switch必备|我看谁还没装这5款宝藏APP!
36566666

Switch必备|我看谁还没装这5款宝藏APP!

📅 07-23 👁️ 7158