null! 是什么意思?声明是什么意思?

What does null! statement mean?

我最近看到了以下代码:

public class Person
{
    //line 1
    public string FirstName { get; }

    //line 2
    public string LastName { get; } = null!;

    //assign null is possible
    public string? MiddleName { get; } = null;

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

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

基本上,我尝试深入研究新的 c# 8 功能。其中之一是 NullableReferenceTypes
实际上,已经有很多关于它的文章和信息。例如。 this article 挺好的。
但是我没有找到关于这个新声明的任何信息 null!
有人可以给我解释一下吗?
为什么我需要使用这个?
line1line2 有什么区别?

理解 null! 含义的关键是理解 ! 运算符。 您以前可能将其用作“非”运算符。但是,由于 C# 8.0 及其新的 "nullable-reference-types" feature, the operator got a second meaning. It can be used on a type to control Nullability, it is then called the "Null Forgiving Operator"


典型用法

假设这个定义:

class Person
{
    // Not every person has a middle name. We express "no middle name" as "null"
    public string? MiddleName;
}

用法为:

void LogPerson(Person person)
{
    Console.WriteLine(person.MiddleName.Length);  // WARNING: may be null
    Console.WriteLine(person.MiddleName!.Length); // No warning
}

此运算符基本上关闭了编译器对此用法的空检查。

技术说明

空安全

C# 8.0 试图帮助您管理您的 null 值。默认情况下,他们不再允许您将 null 分配给所有内容,而是翻转过来,现在要求您 明确地 标记您希望能够持有 [=16] 的所有内容=]值。

这是一个非常有用的功能,它可以让您通过强迫您做出决定并强制执行来避免 NullReferenceExceptions。

工作原理

在谈论空安全时,变量可以处于 2 种状态。

  • 可为空 - 可以为空。
  • 不可为空 - 不能为空。

Since C# 8.0 all reference types are non-nullable by default. Value types have been non-nullable since C# 2.0!

可以通过 2 个新的(类型级)运算符修改“可空性”:

  • ! = 从 NullableNon-Nullable
  • ? = 从 Non-NullableNullable

这些运算符是彼此对应的。 编译器使用您用这些运算符定义的信息来确保空值安全。

例子

? 运算符用法。

此运算符告诉编译器变量可以包含空值。

  • 可为空 string? x;

    • x 是引用类型 - 所以默认情况下不可为空。
    • 我们应用 ? 运算符 - 这使其可以为空。
    • x = null 工作正常。
  • 不可为空 string y;

    • y 是引用类型 - 因此默认情况下不可为空。
    • y = null 生成警告,因为您将 null 值分配给不应为 null 的内容。

很高兴知道:使用 string?System.Nullable<string>

的语法糖

! 运算符用法。

此运算符告诉编译器可以安全访问某些可能为 null 的内容。 您表达了在这种情况下“不关心”空安全的意图。

string x;
string? y;
  • x = y
    • 非法! Warning: "y" may be null
    • 赋值的左侧不可为空,但右侧可为空。
    • 所以它没有工作,因为它在语义上不正确
  • x = y!
    • 合法!
    • y 是应用了 ? 类型修饰符的引用类型,因此如果没有其他证明,它可以为 null。
    • 我们将 ! 应用于 y,后者 覆盖 其可空性设置以使其 不可为空
    • 赋值的右侧和左侧不可为空。这在语义上是正确的。

WARNING The ! operator only turns off the compiler-checks at a type-system level - At runtime, the value may still be null.

谨慎使用!

您应该尝试避免 使用 Null-Forgiving-Operator,使用可能是系统设计缺陷的症状,因为 它会抵消影响编译器会保证空安全性。

推理

使用 ! 运算符将很难找到错误。 如果您有一个标记为不可为空的 属性,您会假设你可以安全地使用它。但是在运行的时候,你突然运行变成了NullReferenceException,挠了挠头。由于在绕过 !.

的编译器检查后,值实际上变成了 null

为什么会有这个运算符?

存在适合使用的有效用例(在下面详细概述)。然而,在 99% 的情况下,您最好使用替代解决方案。请不要在您的代码中打几十个 !,只是为了消除警告。

  • 在某些(边缘)情况下,编译器无法检测到可为 null 的值实际上是不可为 null 的。
  • 遗留代码库迁移更轻松。
  • 在某些情况下,您根本不在乎某些内容是否变为 null。
  • 使用单元测试时,您可能希望在 null 通过时检查代码的行为。

好吗!?但是 null! 是什么意思?

它告诉编译器 null 不是 nullable 值。听起来很奇怪,不是吗?

与上面示例中的 y! 相同。 它看起来很奇怪,因为您将运算符应用于 null 文字 。但概念是一样的。在这种情况下,null 文字与任何其他文字相同 expression/type/value/variable.

null 文字类型是唯一默认可为空的类型!但是正如我们了解到的,any 类型的可空性可以用 ! 覆盖为不可空。

类型系统不关心变量的 actual/runtime 值。只有它的编译时类型以及在您的示例中要分配给 LastName (null!) 的变量是 non-nullable,就类型系统而言,它是有效的。

考虑这段(无效的)代码。

object? null;
LastName = null!;

启用 "nullable reference types" 功能后,编译器会跟踪代码中它认为可能为 null 或不为 null 的值。有时编译器可能知识不足。

例如,您可能正在使用延迟初始化模式,其中构造函数不会使用实际(非空)值初始化所有字段,但您总是调用一个初始化方法来保证字段是非空的无效的。在这种情况下,您面临权衡:

  • 如果您将该字段标记为可为空,编译器会很高兴,但您必须在使用该字段时不必要地检查是否为空,
  • 如果您将该字段保留为不可空,编译器会抱怨它没有被构造函数初始化(您可以使用 null! 抑制它),然后可以在不进行空检查的情况下使用该字段。

请注意,通过使用 ! 抑制运算符,您需要承担一些风险。想象一下,您实际上并没有像您想象的那样一致地初始化所有字段。然后使用 null! 来初始化字段掩盖了 null 正在滑入的事实。一些毫无戒心的代码可能会收到 null 并因此失败。

更一般地说,您可能具有一些领域知识:"if I checked a certain method, then I know that some value isn't null":

if (CheckEverythingIsReady())
{
   // you know that `field` is non-null, but the compiler doesn't. The suppression can help
   UseNonNullValueFromField(this.field!);
}

同样,您必须确信代码的不变性才能执行此操作 ("I know better")。

null!用于将null赋值给不可为null的变量,这是一种承诺变量在实际使用时不会null的方式。

我会在 Visual Studio 扩展中使用 null!,其中属性由 MEF 通过反射初始化:

[Import] // Set by MEF
VSImports vs = null!;
[Import] // Set by MEF
IClassificationTypeRegistryService classificationRegistry = null!; 

(我讨厌变量在这个系统中如何神奇地获取值,但事实就是如此。)

我还在单元测试中使用它来标记由设置方法初始化的变量:

public class MyUnitTests
{
    IDatabaseRepository _repo = null!;

    [OneTimeSetUp]
    public void PrepareTestDatabase()
    {
        ...
        _repo = ...
        ...
    }
}

如果你在这种情况下使用null!,你每次读取变量时都必须使用感叹号,这将是徒劳无益。

如果不使用特定的 C# 版本,我不认为可以讨论这个问题。

public string RpyDescription { get; set; } = null!;

这在 .NET 6 Core 中很常见……事实上,如果您想将字符串描述为不可为空,则这是必需的。存在这种情况的一个原因是当一个人正在使用 SQL 数据库时,其中一个字段可以设置为“允许空值”。在使用接受 null 的 JSON 结构时更是如此。 EF 需要知道。

值类型(堆栈 - 存储数据的内存位置)的引用类型(堆 - 指向存储数据的内存位置的指针)。

.NET 6 (C#10) 默认为项目模板启用可空上下文(在此之前默认禁用可空上下文)。

在EF/Core中,了解数据库空值和model/entities空值之间的关系非常重要。