Mat*_*son 81 c# nullable-reference-types
考虑以下代码:
using System;
#nullable enable
namespace Demo
{
public sealed class TestClass
{
public string Test()
{
bool isNull = _test == null;
if (isNull)
return "";
else
return _test; // !!!
}
readonly string _test = "";
}
}
Run Code Online (Sandbox Code Playgroud)
当我构建它时,标有的行!!!给出了编译器警告:warning CS8603: Possible null reference return.。
我觉得这有点令人困惑,因为它_test是只读的并且初始化为非空。
如果我将代码更改为以下内容,警告就会消失:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
Run Code Online (Sandbox Code Playgroud)
谁能解释这种行为?
Jon*_*eet 62
我可以对这里发生的事情做出合理的猜测,但这有点复杂:) 它涉及规范草案中描述的空状态和空跟踪。从根本上说,在我们想要返回的地方,如果表达式的状态是“maybe null”而不是“not null”,编译器会发出警告。
这个答案是某种叙述形式,而不仅仅是“这是结论”……我希望这样更有用。
我将通过删除字段来稍微简化示例,并考虑具有以下两个签名之一的方法:
public static string M(string? text)
public static string M(string text)
Run Code Online (Sandbox Code Playgroud)
在下面的实现中,我为每个方法指定了不同的编号,以便我可以明确地引用特定示例。它还允许所有实现都出现在同一个程序中。
在下面描述的每种情况下,我们会做各种事情,但最终会尝试返回text- 所以它的 null 状态text很重要。
首先,让我们尝试直接返回它:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Run Code Online (Sandbox Code Playgroud)
到此为止,就这么简单。方法开始时参数的可空状态如果是 type 则为“maybe null”,如果它是 typestring?则为“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
}
}
Run Code Online (Sandbox Code Playgroud)
太好了,所以看起来就像在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
}
}
Run Code Online (Sandbox Code Playgroud)
无论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
}
Run Code Online (Sandbox Code Playgroud)
请注意 M8 感觉它应该与 M2 等效 - 两者都有一个它们无条件返回的非空参数 - 但引入与 null 的比较将状态从“not null”更改为“maybe 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
}
Run Code Online (Sandbox Code Playgroud)
请注意该return语句现在如何没有警告:执行后的状态text.Length为“非空”(因为如果我们成功执行该表达式,则它不可能为空)。因此,text参数由于其类型而以“not null”开头,由于 null 比较而变为“maybe 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
}
Run Code Online (Sandbox Code Playgroud)
因此,即使x is object现在是 的推荐替代方案x != null,它们也没有相同的效果:只有与 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
}
Run Code Online (Sandbox Code Playgroud)
即使我们知道这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);
}
Run Code Online (Sandbox Code Playgroud)
即使我们知道代码将完全输入这些if语句体之一,但规范中没有任何内容可以解决这个问题。静态分析工具很可能能够做到这一点,但是尝试将其放入语言规范中将是一个坏主意,IMO - 静态分析工具具有可以随时间演变的各种启发式方法很好,但不是那么多对于语言规范。
小智 51
可空流分析跟踪变量的空状态,但不跟踪其他状态,例如bool变量的值(isNull如上),也不跟踪单独变量(例如isNull和_test)的状态之间的关系。
实际的静态分析引擎可能会做这些事情,但在某种程度上也会是“启发式的”或“任意的”:您不一定能说出它遵循的规则,这些规则甚至可能会随着时间而改变。
这不是我们可以直接在 C# 编译器中做的事情。可空警告的规则非常复杂(正如 Jon 的分析所示!),但它们是规则,可以推理。
当我们推出该功能时,感觉我们大部分时间都达到了正确的平衡,但有一些地方确实很尴尬,我们将在 C# 9.0 中重新审视这些地方。
Eri*_*ert 31
您已经发现证据表明,在跟踪局部变量中编码的含义时,产生此警告的程序流算法相对简单。
我对流检查器的实现没有特定的了解,但过去曾研究过类似代码的实现,我可以做出一些有根据的猜测。流检查器可能会在误报情况下推断出两件事:(1)_test可能为空,因为如果不能,您将无法进行比较,以及(2)isNull可能为真或为假——因为如果它不能,你就不会在if. 但是return _test;唯一运行的连接如果_test不为空,则不会建立该连接。
这是一个非常棘手的问题,您应该预计编译器需要一段时间才能达到专家多年工作的工具的复杂程度。例如,Coverity 流检查器在推断您的两个变体都没有空返回时完全没有问题,但是 Coverity 流检查器对企业客户来说花费了大量资金。
此外,Coverity 检查器旨在在一夜之间在大型代码库上运行;C# 编译器的分析必须在编辑器中的击键之间运行,这显着改变了您可以合理执行的深入分析的种类。
小智 11
所有其他答案几乎完全正确。
如果有人好奇,我尝试在https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947 中尽可能明确地说明编译器的逻辑
没有提到的一点是我们如何决定是否应该将空检查视为“纯”,从某种意义上说,如果您这样做,我们应该认真考虑空检查是否有可能。在 C# 中有很多“附带”空检查,您在其中测试空值作为做其他事情的一部分,因此我们决定将检查范围缩小到我们确定人们有意进行的检查。我们想出了启发式是“包含单词空”,所以这就是为什么x != null和x is object产生不同的结果。