我可以对这里发生的事情做出合理的猜测,但这有点复杂:) 它涉及null state and null tracking described in the draft spec。从根本上说,在我们想要返回的地方,如果表达式的状态是“可能为空”而不是“非空”,编译器会发出警告。
这个答案有点叙述形式,而不仅仅是“这是结论”......我希望这样更有用。
我将通过去掉字段来稍微简化示例,并考虑使用以下两个签名之一的方法:
public static string M(string? text)
public static string M(string text)
在下面的实现中,我为每种方法指定了不同的编号,因此我可以明确地参考具体示例。它还允许所有实现都出现在同一个程序中。
在下面描述的每种情况下,我们都会做各种事情,但最终会尝试返回 text - 所以 text 的 null 状态很重要。
无条件返回
首先,我们直接尝试返回:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
到目前为止,一切都很简单。如果参数的类型为string?,则方法开头的参数的可空状态为“可能为空”,如果其类型为string,则为“非空”。
简单的条件返回
现在让我们检查if 语句条件本身的空值。 (我会使用条件运算符,我相信它会产生相同的效果,但我想更真实地回答这个问题。)
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
}
}
M5 和 M6 都会发出警告。因此,我们不仅没有得到 M5 中状态从“可能为空”变为“非空”的积极影响(就像我们在 M3 中所做的那样)......我们在 M6 中得到了相反效果,状态从“not null”变为“maybe null”。这让我很惊讶。
看来我们已经了解到:
- 围绕“如何计算局部变量”的逻辑不用于传播状态信息。稍后会详细介绍。
- 引入 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 语句现在没有警告:执行text.Length 之后 的状态是“not null”(因为如果我们成功执行该表达式,它就不能为空)。因此text 参数因其类型而以“not null”开始,由于 null 比较而变为“maybe null”,然后在text2.Length 之后再次变为“not null”。
哪些比较会影响状态?
所以这是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 的推荐替代品,它们的效果也不相同:仅与 null 比较(与 is、@987654346 中的任何一个进行比较@ 或 !=) 将状态从“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
}
尽管我们知道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);
}
尽管我们知道代码将准确地输入其中一个if 语句体,但规范中没有任何内容可以解决这个问题。静态分析工具很可能能够做到这一点,但试图将其纳入语言规范将是一个坏主意,IMO - 静态分析工具可以拥有各种可以随时间演变的启发式方法,但不是那么多用于语言规范。