为什么我会收到这些奇怪的C#8“可空引用类型”警告:非空字段在退出构造函数之前必须包含非空值。?
c#
c#-8.0
我正在尝试处理 C# 8 的新可空编译器检查。以下结构给了我一些奇怪的警告。
// This struct gives no warnings
public struct Thing<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
Thing(T value)
{
_value = value;
_hasValue = value is { };
}
}
// This struct gives warning on the constructor:
// "Non-nullable field "_value" must contain a non-null value before exiting constructor.
// Consider declaring the field as nullable.
public struct Thing<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
Thing(T value)
~~~~~
{
_value = value;
_hasValue = _value is { };
}
}
// This struct gives two warnings, one on the constructor and one on `_value = value`
// [1] "Non-nullable field "_value" must contain a non-null value before exiting constructor.
// Consider declaring the field as nullable.
// [2] Possible null reference assignment.
// This is true even if I check value for null and throw an ArgumentNullException before the assignment.
public struct Thing<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
Thing(T value)
~~~~~ // [1]
{
_hasValue = value is { };
_value = value;
~~~~~ // [2]
}
}
我是否创造了一些不可能的情况,编译器无法弄清楚意图?这是与构造函数中的可为空引用类型相关的编译器错误吗?我在这里缺少什么?
回答
所有这些场景对我来说都很有意义。让我们看看每一个,但首先让我们就一些事情达成一致:
-
不可为空/可为空的引用类型不是真正的类型。它们只是编译器知道如何解释的注释
-
该结构被限制为
notnull但是有人可以传递null对它的引用,如果他们没有启用可空分析(#nullable enable),则不会发出任何警告 -
val is {}是一个null测试。通过使用它,您暗示该值可以是null -
编译器使用流分析。它寻找各种模式来确定值的状态。它将把你的话,有些事情是/不是
null,它会用以前的任务为变量的空性的证据,即使它可能没有什么意义- 这是这里的关键。一个未完全构造的对象的字段可以从一行更改为下一行,这对我们来说没有意义;没有两个线程改变值。但是编译器不是人类聪明的,当我们告诉它我们知道得更好时,它会听从我们
-
在大多数(所有?)情况下,流分析不会向后工作,这意味着在线断言状态
N不会影响线N-1
让我们看一下例子:
public struct ThingA<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
ThingA(T value)
{
_value = value;
_hasValue = value is { };
}
}
ThingA不发出警告。您已约束notnull并分配给_value. 在此之前,您还没有执行任何可空性断言,因此编译器假定到目前为止该参数实际上不是null 每个 contract。流分析不会向后工作。分配给_hasValue使用null测试对参数只会影响未来的可为空的状态value 参数。编译器不会说“嘿,我记得value用来分配某些东西,让我去修复所有东西”——那样工作太多了。现在我们退出构造函数并且您没有重新分配该_value字段,因此其先前确定的“非空”状态仍然存在。没有警告。
public struct ThingB<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
ThingB(T value)
{
_value = value;
_hasValue = _value is { };
}
}
ThingB从与 类似的流程开始ThingA。因为我们没有以前的null测试并且我们notnull对 T有约束,所以分配给该_value字段会根据合同加强其非空状态。但这就是它变得棘手的地方!您现在执行null对非常相同的现场测试,从而暗示它可能实际上是null。流分析不会向后工作,因此我们不会在分配给该字段的行上收到警告,但您已告诉编译器该字段的状态可能是null. 我们正在退出带有可能null字段的构造函数。这是你的警告。
public struct ThingC<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
ThingC(T value)
{
_hasValue = value is { };
_value = value;
}
}
对于ThingC,您null对参数执行检查,暗示它可以是null。参数的null状态更改为“可能为空”,然后在分配给 时使用该参数_value。嗯,你刚才说的它可以是null,但每个约束它不能。这是第一个警告。现在我们离开构造函数体,字段的状态仍然是“可能为空”(根据赋值)。这是第二个警告。
在你说的评论中
即使我检查 null 值并在赋值之前抛出 ArgumentNullException 也是如此。
好吧,这取决于您将支票放在哪里。考虑一下:
public struct ThingD<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
ThingD(T value)
{
if (value is null) throw new Exception();
_hasValue = value is { };
_value = value;
}
}
在正常情况下,流分析会捕捉到这一点。除本遭受了同样的问题ThingC:你告诉编译器,这个参数还是(!不知)可以是null你的_hasValue任务/null测试。你会得到同样的警告。夏普实验室
如果在测试后移动null-check plus 异常,则不会收到警告:_hasValue
public struct ThingE<T> where T : notnull
{
private readonly bool _hasValue;
private readonly T _value;
ThingE(T value)
{
_hasValue = value is { };
if (value is null) throw new Exception();
_value = value;
}
}
处理时ThingE,没有警告。异常保护对_value已断言参数 is not的字段的赋值null。尽管_hasValue赋值暗示它可以。你是说“从这一点上它绝对不是null”。夏普实验室
请记住,可为空的注释和约束不是类型系统的“真实”部分。传递可空值甚至null直接传递都不会产生编译错误,只会产生警告(当然除非TreatWarningsAsErrors启用)。还请记住,您的期望非null参数的类/方法可能会被欺骗,无论是通过
- 禁用(或只是不启用)
#nullable - 使用
null-forgiving 运算符,! - 忽略警告(根据我的经验,很多人都这样做)
您仍然必须后卫null,通常检查和投掷右远。这将有助于进行大量的流量分析。流量分析并不完美。有很多模式应该可以检测到但不能检测,并且团队一直在努力使其更好(C#9 有一些改进)。
有趣的是,当可空引用类型第一次出现时,我真的不确定如果我们使用非类型是否真的需要执行null测试null。2019 年,在他的一次 C#8 演讲之后,我在 Microsoft Ignite 上与 Mads Torgersen 进行了一次相当长的对话。他同意我的看法,即这个主题可能不清楚,而且(至少在当时)甚至文件也没有让它变得明显。他强调,除非类型是内部的——所以,作为公共 API 的一部分——否则执行null保护前置条件测试仍然是必要的。