无效是什么!声明是什么意思?


76

我最近看过以下代码:

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。实际上,已经有很多文章和信息。例如,这篇文章相当不错。但是我找不到有关此新声明的任何信息,null! 有人可以为我提供解释吗?为什么我需要使用这个?又什么区别line1line2


15
!是可以为null的运算符,它告诉编译器,尽管通常它不允许这样做,但它应该以其他方式查找并允许它,因为我们知道得更多。null!本身几乎没有实际用途,因为它全部否定了可空引用类型的用途。当您知道表达式不能为时null,它会更有用,而编译器则不能。
Jeroen Mostert

1
@JeroenMostert如此喜欢力量?我的意思是,即使不寻常,还是让我们强制这样做。
isxaker

2
是的,除了它不寻常以外-因为string在新规则下,它不是可为null的引用类型,因此永远不应为null nullnull!有效地分配说:“我知道这绝对不应该null,但是猜猜我在做什么”。几乎没有哪个程序是有意义的-这样做的唯一原因是因为您知道null在任何人都可以得到之前要分配一个非值NullReferenceException,并希望表示您没有忘记分配。它。可能(但不太可能),因此,它不是一个很好的例子。
Jeroen Mostert

是什么意思public string? MiddleName {get; } = null;?为什么将字符串设为可空值?它不是已经可以为null了吗?还是我错过了什么?
伊马德(Imad)

1
@JeroenMostert我找到了空值!在单元测试中很有用。仅仅因为引用不应该为null并不一定意味着它就不会为null,所以有时在这种情况下测试正确的行为是适当的。
杰西

Answers:


95

了解null!含义的关键是了解!操作员。您之前可能已将其用作“非”运算符。但是从C#8.0开始,该运算符还可以用于控制Nullabilty的类型

!在类型上使用什么运算符?

!运算符用于某种类型时,称为Null宽容运算符[ docs ]。它是在C#8.0中引入的


技术说明

典型用法

假设此定义:

class Person
{
  public string? MiddleName;
}

用法是:

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

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

内部运作

使用此运算符可以告诉编译器可以安全地访问可能为空的内容。您表示打算在这种情况下“不关心”空安全性。

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

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

从C#8.0开始,默认情况下所有引用类型都是不可为空的。

可以通过以下两个新的类型操作符来修改“可空性”:

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

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

? 操作员用法。

  1. 可空 string? x;

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

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

! 操作员用法。

string x;
string? y = null;
  1. x = y

    • 非法!--Warning: "y" may be null
    • 分配的左侧为不可为空,而右侧则为可为空。
  2. x = y!

    • 法律!
    • 分配的左侧和右侧不可为空。
    • 自从y!!运算符应用于y使其不可为空的运算符以来,一直有效。

警告!操作者仅关闭在一个类型的系统级的编译器检查-在运行时,仍然值可以为空。

这是一个反模式。

你应该尝试避免使用!空赦的运营商。

在此操作员适合使用的情况下,有有效的用例(在下面详细介绍),例如单元测试。不过,在99%的情况下,使用替代解决方案会更好。请不要!在您的代码中拍打几十个,只是为了使警告静音。考虑一下您的原因是否真的值得使用。

使用-但要小心。如果没有具体目的/用例,请不要使用它。

它抵消了编译器保证的空安全性的影响。

使用!运算符将很难发现错误。如果您有一个标记为不可为空的属性,则将假定您可以安全地使用它。但是在运行时,您突然遇到了一个问题NullReferenceException并且抓了头。由于在绕过带有的编译器检查之后,值实际上变为null !

那么为什么这个运算符存在?

  • 在某些情况下,编译器无法检测到可为空的值实际上是不可为空的。
  • 遗留代码库迁移更容易。
  • 在某些情况下,您根本不在乎某些东西是否为空。
  • 使用单元测试时,您可能需要检查null通过时的代码行为。

具体回答您的问题。

那是什么null!意思呢?

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

y!上面的示例相同。因为您将运算符应用于null文字,所以它看起来很奇怪。但是概念是相同的。

整理正在发生的事情。

public string LastName { get; } = null!;

该行定义了一个名为LastNametype的不可为空的类属性string。由于它是不可为空的,因此从技术上讲,您不能为它分配null。

但是您只需要使用运算符就可以做到这一点-分配nullLastName- !。因为null!它不是null-就编译器所关心的null安全而言。


1
字符串可以为null。您可以像这样的字符串赋空值str = null; 我正在使用c#6 .net 4.6.1。我认为您的陈述是错误的“此行定义了一个不可空的类属性,名为LastNameof类型字符串。由于它是不可空的,因此从技术上讲,您不能为它分配空值-显然。”
canbax

7
@canbax Nullabilty检查仅受c#8及更高版本支持
Patrick Hollweck

3
@canbaxa string could be null不再。我的意思是如果您使用c# 8并启用NullableReferenceTypes功能。如果您尝试将null分配给,VS会立即警告您string。但是从另一方面来说,您可以引入可为空的string(string? s = null)。在这种情况下,没有警告。
isxaker

3
“您应该尝试永远不要使用!Null-Forgiving-Operator。它会抵消编译器保证的null安全性的影响。” 您将如何编写用于参数验证的单元测试?这是我最常使用的!在野田时间。
乔恩·斯基特

1
绝对会建议以后再编辑-当前,在您的测试中广泛使用null宽容运算符的任何人,在您使用当前文本的情况下,都可能很容易对此感到不适,但这并不能说明建议的局限性。我认为在几个代码库中都不可能实现“从不”-例如,在Noda Time中,解析结果具有不变性,这意味着我永远不会暴露空值,但是我仍然会得到解析本身失败的结果。我认为“努力避免”具有更积极的基调。不过只是我的意见。
乔恩·斯基特

14

启用“可空引用类型”功能后,编译器会跟踪代码中它认为可能为null或不为null的值。有时编译器可能没有足够的知识。

例如,您可能正在使用延迟初始化模式,在该模式下,构造函数未使用实际(非空)值初始化所有字段,但是您始终调用一种初始化方法来确保这些字段为非空值。在这种情况下,您将面临一个折衷:

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

请注意,通过使用!抑制运算符,您将承担一些风险。想象一下,您实际上并未像您想象的那样一致地初始化所有字段。然后使用null!初始化字段掩盖了a插入的事实null。一些毫无疑问的代码会收到anull并因此失败。

更一般而言,您可能具有一些领域知识:“如果我检查了某种方法,那么我知道某些值不为空”:

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

同样,您必须确信自己的代码是不变的,可以做到这一点(“我知道得更多”)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.