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!
有人可以给我解释一下吗?
为什么我需要使用这个?
line1
和 line2
有什么区别?
理解 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] 的所有内容=]值。
这是一个非常有用的功能,它可以让您通过强迫您做出决定并强制执行来避免 NullReferenceException
s。
工作原理
在谈论空安全时,变量可以处于 2 种状态。
- 可为空 - 可以为空。
- 不可为空 - 不能为空。
Since C# 8.0 all reference types are non-nullable by default.
Value types have been non-nullable since C# 2.0!
可以通过 2 个新的(类型级)运算符修改“可空性”:
!
= 从 Nullable
到 Non-Nullable
?
= 从 Non-Nullable
到 Nullable
这些运算符是彼此对应的。
编译器使用您用这些运算符定义的信息来确保空值安全。
例子
?
运算符用法。
此运算符告诉编译器变量可以包含空值。
可为空 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空值之间的关系非常重要。
我最近看到了以下代码:
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!
有人可以给我解释一下吗?
为什么我需要使用这个?
line1
和 line2
有什么区别?
理解 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] 的所有内容=]值。
这是一个非常有用的功能,它可以让您通过强迫您做出决定并强制执行来避免 NullReferenceException
s。
工作原理
在谈论空安全时,变量可以处于 2 种状态。
- 可为空 - 可以为空。
- 不可为空 - 不能为空。
Since C# 8.0 all reference types are non-nullable by default. Value types have been non-nullable since C# 2.0!
可以通过 2 个新的(类型级)运算符修改“可空性”:
!
= 从Nullable
到Non-Nullable
?
= 从Non-Nullable
到Nullable
这些运算符是彼此对应的。 编译器使用您用这些运算符定义的信息来确保空值安全。
例子
?
运算符用法。
此运算符告诉编译器变量可以包含空值。
可为空
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
,挠了挠头。由于在绕过 !
.
为什么会有这个运算符?
存在适合使用的有效用例(在下面详细概述)。然而,在 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空值之间的关系非常重要。