我可以做出合理的guess至于这里发生了什么,但这有点复杂:)它涉及到规范草案中描述的空状态和空跟踪 https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/nullable-reference-types-specification#null-state-and-null-tracking。从根本上来说,在我们想要返回的地方,如果表达式的状态是“maybe null”而不是“not null”,编译器会发出警告。
这个答案是某种叙述性的形式,而不仅仅是“这是结论”......我希望这样更有用。
我将通过删除字段来稍微简化示例,并考虑具有这两个签名之一的方法:
public static string M(string? text)
public static string M(string text)
在下面的实现中,我为每个方法指定了不同的编号,这样我就可以明确地参考具体示例。它还允许所有实现出现在同一个程序中。
在下面描述的每种情况下,我们都会做各种事情,但最终会尝试返回text
- 所以这是空状态text
这很重要。
无条件退货
首先,我们尝试直接返回它:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
到目前为止,就这么简单。如果该参数的类型为“可能为空”,则该方法开始时参数的可为空状态为“可能为空”string?
如果它是类型,则为“not null”string
.
简单的条件返回
现在让我们检查一下是否为 nullif
语句条件本身。 (我会使用条件运算符,我相信它会产生相同的效果,但我想更真实地回答这个问题。)
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
太好了,所以它看起来像在一个if
语句,其中条件本身检查无效性,即每个分支内变量的状态if
声明可以不同:在else
块中,两段代码中的状态均为“非空”。因此,特别是在 M3 中,状态从“可能为空”变为“不为空”。
带局部变量的条件返回
现在让我们尝试将该条件提升到局部变量:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
BothM5和M6发出警告。因此,我们不仅没有得到 M5 中状态从“可能为空”到“不为空”变化的积极影响(就像我们在 M3 中所做的那样)……我们得到了oppositeM6 中的效果,其中状态从“不为空”变为“可能为空”。这真的让我很惊讶。
所以看起来我们已经了解到:
- 围绕“如何计算局部变量”的逻辑不用于传播状态信息。稍后会详细介绍。
- 引入 null 比较可以警告编译器,它之前认为不为 null 的内容最终可能为 null。
忽略比较后无条件返回
让我们通过在无条件返回之前进行比较来看看第二个要点。 (所以我们完全忽略比较的结果。):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
请注意 M8 感觉它应该与 M2 等效 - 两者都有一个非空参数,它们无条件返回 - 但引入与 null 的比较将状态从“非空”更改为“可能为空”。我们可以通过尝试取消引用来获得进一步的证据text
条件之前:
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
请注意如何return
声明现在没有警告:国家after执行text.Length
是“not null”(因为如果我们成功执行该表达式,它就不可能为null)。所以text
参数由于其类型而开始为“not null”,由于 null 比较而变为“maybe null”,然后再次变为“not null”text2.Length
.
哪些比较会影响状态?
所以这是一个比较text is null
...类似的比较有什么效果?这里还有另外四种方法,全部以不可为空的字符串参数开头:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
所以尽管x is object
现在是推荐的替代品x != null
,它们没有相同的效果:只是比较与空(与任何is
, ==
or !=
) 将状态从“not null”更改为“maybe null”。
为什么提升条件会产生效果?
回到我们之前的第一个要点,为什么 M5 和 M6 不考虑导致局部变量的条件?这并不让我感到惊讶,但似乎让其他人感到惊讶。将这种逻辑构建到编译器和规范中需要大量工作,而且收益相对较小。这是另一个与可空性无关的示例,其中内联某些内容会产生影响:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
虽然we我知道alwaysTrue
将始终为真,它不满足规范中使代码之后的要求if
声明不可达,这正是我们所需要的。
这是另一个关于明确赋值的例子:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
虽然we知道代码将准确输入其中之一if
声明体,规范中没有任何内容可以解决这个问题。静态分析工具很可能能够做到这一点,但试图将其放入语言规范中将是一个坏主意,IMO - 静态分析工具拥有各种可以随时间演变的启发式方法很好,但不是那么多用于语言规范。