我可以对这里发生的事情做出一个合理的猜测,但这有点复杂:)它涉及规范草案中描述的null状态和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
到目前为止,如此简单。在方法开始时,参数的可空状态为“可能为null”(如果类型为),string?
而为“ not null”(类型为)string
。
简单的有条件回报
现在,让我们检查if
语句条件本身内是否为null 。(我将使用条件运算符,我相信它将产生相同的效果,但我想对这个问题保持真实。)
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中效果,其中状态从“不为空”到“可能为空”。真的让我感到惊讶。
看来我们已经了解到:
- 围绕“如何计算局部变量”的逻辑不用于传播状态信息。以后再说。
- 引入空比较可以警告编译器,它以前认为不为空的内容可能最终还是为空。
比较被忽略后无条件返回
让我们通过无条件返回之前的比较来看看其中的第二点。(因此,我们完全忽略了比较结果。):
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-两者都具有一个非空参数,它们无条件返回-但是引入空值比较会将状态从“非空值”更改为“可能为空值”。我们可以通过尝试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
为“不为空”(因为如果成功执行该表达式,则不能为空)。因此text
参数由于其类型而从“ not null”开始,由于null比较而变为“ may 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
,但它们不会产生相同的效果:仅与null(带有is
,==
或!=
)可以将状态从“ not null”更改为“也许为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-静态分析工具具有可以随时间发展而来的各种启发式方法是很好的,但不是那么多语言规范。