C 中的可以为 null 的引用类型#Nullable reference types in C#

此功能的目标是:The goal of this feature is to:

  • 允许开发人员表达引用类型的变量、参数或结果是否应为空。Allow developers to express whether a variable, parameter or result of a reference type is intended to be null or not.
  • 当不根据此目的使用此类变量、参数和结果时提供警告。Provide warnings when such variables, parameters and results are not used according to that intent.

意向表达式Expression of intent

该语言已包含 T? 值类型的语法。The language already contains the T? syntax for value types. 向引用类型扩展此语法非常简单。It is straightforward to extend this syntax to reference types.

假设未修饰引用类型的目的 T 是将其视为非 null。It is assumed that the intent of an unadorned reference type T is for it to be non-null.

检查可以为 null 的引用Checking of nullable references

流分析跟踪可以为 null 的引用变量。A flow analysis tracks nullable reference variables. 如果分析认为它们不会为 null (例如,在检查或赋值) 之后,则将其值视为非空引用。Where the analysis deems that they would not be null (e.g. after a check or an assignment), their value will be considered a non-null reference.

可以为 null 的引用使用后缀 x! 运算符( (为 "damnit" 运算符) )显式处理为非 null,这是因为流分析无法建立开发人员知道的非 null 情况。A nullable reference can also explicitly be treated as non-null with the postfix x! operator (the "damnit" operator), for when flow analysis cannot establish a non-null situation that the developer knows is there.

否则,如果取消引用可为 null 的引用或者将其转换为非 null 类型,则会发出警告。Otherwise, a warning is given if a nullable reference is dereferenced, or is converted to a non-null type.

当从转换到时,将发出警告 S[] T?[] S?[] T[]A warning is given when converting from S[] to T?[] and from S?[] to T[].

从转换到时,将发出 C<S> 警告 C<T?> ,但当类型参数是协变 (out) 时,以及从转换 C<S?> 到( C<T> 当类型参数是逆变 () 时除外) inA warning is given when converting from C<S> to C<T?> except when the type parameter is covariant (out), and when converting from C<S?> to C<T> except when the type parameter is contravariant (in).

C<T?>如果类型参数具有非 null 约束,则会发出警告。A warning is given on C<T?> if the type parameter has non-null constraints.

检查非空引用Checking of non-null references

如果将 null 文本分配给非 null 变量或作为非 null 参数传递,则会发出警告。A warning is given if a null literal is assigned to a non-null variable or passed as a non-null parameter.

如果构造函数未显式初始化非空引用字段,则还会发出警告。A warning is also given if a constructor does not explicitly initialize non-null reference fields.

我们无法充分跟踪非空引用数组的所有元素是否已初始化。We cannot adequately track that all elements of an array of non-null references are initialized. 但是,如果在从或传入数组之前没有将新创建的数组的元素分配给,我们可能会发出警告。However, we could issue a warning if no element of a newly created array is assigned to before the array is read from or passed on. 这可能会处理常见情况,而不会干扰。That might handle the common case without being too noisy.

我们需要确定是 default(T) 生成警告,还是只将其视为类型 T?We need to decide whether default(T) generates a warning, or is simply treated as being of the type T?.

元数据表示形式Metadata representation

应为空性修饰在元数据中表示为属性。Nullability adornments should be represented in metadata as attributes. 这意味着下层编译器将忽略它们。This means that downlevel compilers will ignore them.

我们需要确定是否只包含可为 null 的批注,或者程序集中是否存在非 null 的指示。We need to decide if only nullable annotations are included, or there's also some indication of whether non-null was "on" in the assembly.

泛型Generics

如果类型参数 T 具有不可为 null 的约束,则该类型参数在其作用域内被视为不可为 null。If a type parameter T has non-nullable constraints, it is treated as non-nullable within its scope.

如果类型参数不受约束或只有可为 null 的约束,则这种情况稍微复杂一些:这意味着相应的类型参数可以 为 null,也可以是 不可为 null 的。If a type parameter is unconstrained or has only nullable constraints, the situation is a little more complex: this means that the corresponding type argument could be either nullable or non-nullable. 在这种情况下要执行的安全操作是将类型 参数视为可以为 null 和 不可为 null,并在违反时发出警告。The safe thing to do in that situation is to treat the type parameter as both nullable and non-nullable, giving warnings when either is violated.

需要考虑是否应允许显式可为 null 的引用约束。It is worth considering whether explicit nullable reference constraints should be allowed. 但请注意,在某些情况下,我们无法避免将可为 null 的引用类型 隐式 约束为 (继承的约束) 。Note, however, that we cannot avoid having nullable reference types implicitly be constraints in certain cases (inherited constraints).

class约束为非 null。The class constraint is non-null. 我们可以考虑是否 class? 应为表示 "可以为 null 的引用类型" 的有效可为 null 的约束。We can consider whether class? should be a valid nullable constraint denoting "nullable reference type".

类型推理Type inference

在类型推理中,如果参与类型是可以为 null 的引用类型,则结果类型应为 null。In type inference, if a contributing type is a nullable reference type, the resulting type should be nullable. 换句话说,非 null 是传播的。In other words, nullness is propagated.

应考虑 null 文本是否为参与表达式应非 null。We should consider whether the null literal as a participating expression should contribute nullness. 目前不是:对于值类型,它会导致错误,而对于引用类型,null 成功转换为纯类型。It doesn't today: for value types it leads to an error, whereas for reference types the null successfully converts to the plain type.

string? n = "world";
var x = b ? "Hello" : n; // string?
var y = b ? "Hello" : null; // string? or error
var z = b ? 7 : null; // Error today, could be int?

空防护指南Null guard guidance

作为一项功能,可为 null 的引用类型允许开发人员表达其意图,并通过流分析提供警告,前提是该意向是冲突的。As a feature, nullable reference types allow developers to express their intent, and provide warnings through flow analysis if that intent is contradicted. 对于是否需要 null 保护,有一个常见的问题。There is a common question as to whether or not null guards are necessary.

Null guard 的示例Example of null guard

public void DoWork(Worker worker)
{
    // Guard against worker being null
    if (worker is null)
    {
        throw new ArgumentNullException(nameof(worker));
    }

    // Otherwise use worker argument
}

在上面的示例中, DoWork 函数接受 Worker ,并防范它的存在 nullIn the previous example, the DoWork function accepts a Worker and guards against it potentially being null. 如果 worker 参数为 null ,则 DoWork 函数将为 throwIf the worker argument is null, the DoWork function will throw. 对于可以为 null 的引用类型,上一示例中的代码将使该 Worker 参数 nullWith nullable reference types, the code in the previous example makes the intent that the Worker parameter would not be null. 如果该 DoWork 函数是一个公共 API,如 NuGet 包或共享库-作为指导,你应将 null 保护保留原样。If the DoWork function was a public API, such as a NuGet package or a shared library - as guidance you should leave null guards in place. 作为公共 API,仅保证调用方不会通过的唯一保证 null 是防止其受到防范。As a public API, the only guarantee that a caller isn't passing null is to guard against it.

快速意向Express intent

上一示例更引人注目的用法是表达 Worker 参数可能是 null ,从而使空防护更合适。A more compelling use of the previous example is to express that the Worker parameter could be null, thus making the null guard more appropriate. 如果在下面的示例中删除了 null guard,则编译器会警告您可以取消引用 null。If you remove the null guard in the following example, the compiler warns that you may be dereferencing null. 无论如何,这两个 null 保护仍然有效。Regardless, both null guards are still valid.

public void DoWork(Worker? worker)
{
    // Guard against worker being null
    if (worker is null)
    {
        throw new ArgumentNullException(nameof(worker));
    }

    // Otherwise use worker argument
}

对于非公共 Api (如开发人员或开发团队完全控制的源代码),可以为 null 的引用类型允许安全移除 null 保护,而开发人员可以保证它不是必需的。For non-public APIs, such as source code entirely in control by a developer or dev team - the nullable reference types could allow for the safe removal of null guards where the developers can guarantee it is not necessary. 此功能可帮助解决警告,但它无法保证在运行时代码执行时可能会导致 NullReferenceExceptionThe feature can help with warnings, but it cannot guarantee that at runtime code execution could result in a NullReferenceException.

中断性变更Breaking changes

非 null 警告是对现有代码的明显重大更改,并应附带有选择的机制。Non-null warnings are an obvious breaking change on existing code, and should be accompanied with an opt-in mechanism.

很显然,如以上所述,可以为 null 的类型 (的警告) 在为空性为隐式的某些情况下,对现有代码进行重大更改:Less obviously, warnings from nullable types (as described above) are a breaking change on existing code in certain scenarios where the nullability is implicit:

  • 不受约束的类型参数将被视为隐式的,因此将其分配给 object 或访问(例如) ToString 将产生警告。Unconstrained type parameters will be treated as implicitly nullable, so assigning them to object or accessing e.g. ToString will yield warnings.
  • 如果类型推理从表达式中推断出非 null null ,则现有代码有时会产生可以为 null 的类型,而不是不可为 null 的类型,这可能会导致新的警告。if type inference infers nullness from null expressions, then existing code will sometimes yield nullable rather than non-nullable types, which can lead to new warnings.

因此,可以为 null 的警告也需要是可选的So nullable warnings also need to be optional

最后,将批注添加到现有 API 将是在升级库时选择了警告的用户的重大更改。Finally, adding annotations to an existing API will be a breaking change to users who have opted in to warnings, when they upgrade the library. 这也可以选择加入或退出。"我想修复 bug,但未准备好处理其新注释"This, too, merits the ability to opt in or out. "I want the bug fixes, but I am not ready to deal with their new annotations"

总之,你需要能够选择加入/退出:In summary, you need to be able to opt in/out of:

  • 可以为 null 的警告Nullable warnings
  • 非 null 警告Non-null warnings
  • 其他文件中的批注警告Warnings from annotations in other files

选择的粒度建议使用类似于分析器的模型,在该模型中,大量的代码可以选择使用杂注,而用户可以选择严重级别。The granularity of the opt-in suggests an analyzer-like model, where swaths of code can opt in and out with pragmas and severity levels can be chosen by the user. 此外,每个库选项 ( "从 JSON.NET 中忽略批注,直到我准备好处理过时" ) 可以在代码中将其表达为属性。Additionally, per-library options ("ignore the annotations from JSON.NET until I'm ready to deal with the fall out") may be expressible in code as attributes.

选择加入/过渡体验的设计对于此功能的成功和有用性至关重要。The design of the opt-in/transition experience is crucial to the success and usefulness of this feature. 我们需要确保:We need to make sure that:

  • 用户可以根据需要逐步采用空性检查Users can adopt nullability checking gradually as they want to
  • 库作者可以添加可为 null 性的注释,而无需担心用户中断Library authors can add nullability annotations without fear of breaking customers
  • 尽管如此,这并不是 "配置不足"Despite these, there is not a sense of "configuration nightmare"

调整Tweaks

我们可以考虑不在 ? 局部变量上使用批注,而只是根据分配给它们的内容来观察它们是否用于。We could consider not using the ? annotations on locals, but just observing whether they are used in accordance with what gets assigned to them. 我不愿意这样做;我想我们应该统一让人们表达自己的意图。I don't favor this; I think we should uniformly let people express their intent.

可以考虑使用参数的速记 T! x ,自动生成运行时 null 检查。We could consider a shorthand T! x on parameters, that auto-generates a runtime null check.

某些泛型类型的模式(如 FirstOrDefaultTryGet )具有不能为 null 的类型参数的轻微行为,因为在某些情况下它们会显式产生默认值。Certain patterns on generic types, such as FirstOrDefault or TryGet, have slightly weird behavior with non-nullable type arguments, because they explicitly yield default values in certain situations. 我们可以尝试对类型系统进行细微差别,以使其更好地适应。We could try to nuance the type system to accommodate these better. 例如,我们可以允许 ? 在不受约束的类型参数上,即使类型参数可以为 null。For instance, we could allow ? on unconstrained type parameters, even though the type argument could already be nullable. 我怀疑这是值得的,这会导致与可为 null 的 类型的交互相关的问题。I doubt that it is worth it, and it leads to weirdness related to interaction with nullable value types.

可以为 null 的值类型Nullable value types

对于可以为 null 的值类型,我们可以考虑采用上述语义。We could consider adopting some of the above semantics for nullable value types as well.

我们已提到类型推理,可在其中推断 int? (7, null) ,而不只是提供错误。We already mentioned type inference, where we could infer int? from (7, null), instead of just giving an error.

另一种机会是将流分析应用于可为 null 的值类型。Another opportunity is to apply the flow analysis to nullable value types. 如果它们被视为非 null,我们实际上可以允许在某些情况下使用作为不可为 null 的类型 (例如,成员访问) 。When they are deemed non-null, we could actually allow using as the non-nullable type in certain ways (e.g. member access). 我们只需小心,你可以在可以为 null 的值类型 上执行的操作就 是出于后向兼容的原因。We just have to be careful that the things that you can already do on a nullable value type will be preferred, for back compat reasons.