为什么当空引用似乎不可能时,我们会收到可能的取消引用空引用警告



在 HNQ 上阅读了这个问题后,我继续阅读了 C# 8 中的可为空引用类型,并进行了一些实验。

我很清楚,当有人说"我发现了一个编译器错误!"时,10次中有9次,甚至更多,这实际上是设计使然,也是他们自己的误解。而且由于我今天才开始研究此功能,因此显然我对它没有很好的理解。说完这些,让我们看一下这段代码:

#nullable enable
class Program
{
static void Main()
{
var s = "";
var b = s == null; // If you comment this line out, the warning on the line below disappears
var i = s.Length; // warning CS8602: Dereference of a possibly null reference
}
}

阅读了我上面链接的文档后,我希望s == null行能给我一个警告——毕竟s显然是不可空的,所以将其与null进行比较是没有意义的。

相反,我在下一行收到警告,警告说s可能是空引用,即使对于人类来说,很明显它不是。

此外,如果我们不将snull进行比较,则不会显示警告。

我做了一些谷歌搜索,我遇到了一个GitHub问题,结果证明这完全是关于其他事情的,但在这个过程中,我与一位贡献者进行了对话,对这种行为提供了更多的见解(例如,"空检查通常是告诉编译器重置其关于变量可空性的先前推理的有用方法。然而,这仍然给我留下了未回答的主要问题。

与其创建一个新的GitHub问题,并可能占用非常繁忙的项目贡献者的时间,我把它发布给社区。

你能解释一下发生了什么,为什么?特别是,为什么s == null行上没有生成警告,为什么当这里似乎无法null引用时,我们CS8602?如果可空性推理不是防弹的,正如链接的 GitHub 线程所暗示的那样,它怎么会出错?会有哪些例子呢?

这实际上是@stuartd链接的答案的副本,所以我不会在这里深入讨论细节。但问题的根源在于,这既不是语言错误也不是编译器错误,而是完全按照实现的预期行为。我们跟踪变量的空状态。最初声明变量时,该状态为 NotNull,因为您使用非 null 的值显式初始化它。但是我们不会跟踪NotNull的来源。例如,这实际上是等效的代码:

#nullable enable
class Program
{
static void Main()
{
M("");
}
static void M(string s)
{
var b = s == null;
var i = s.Length; // warning CS8602: Dereference of a possibly null reference
}
}

在这两种情况下,您都显式测试snull。我们将其作为流量分析的输入,就像 Mads 在这个问题中回答的那样:https://stackoverflow.com/a/59328672/2672518。在该答案中,结果是您在退货时收到警告。在这种情况下,答案是你收到一条警告,指出你取消引用了一个可能为空的引用。

它不会变得可为空,仅仅是因为我们愚蠢地将其与null进行比较。

是的,它确实如此。到编译器。作为人类,我们可以查看此代码,并清楚地理解它不能抛出空引用异常。但是,在编译器中实现可为空的流分析的方式则不能。我们确实讨论了对此分析的一些改进,其中我们根据值的来源添加了其他状态,但我们认为这给实现增加了很多复杂性,但收益并不大,因为这唯一有用的地方是这样的情况, 其中,用户使用new或常量值初始化变量,然后检查它是否null

如果可空性推断不是防弹的,[..] 怎么会出错?

一旦 C#8 的可空引用可用,我就很高兴地采用了它们。当我习惯使用ReSharper的[NotNull](等)符号时,我确实注意到两者之间的一些差异。

C# 编译器可能会被愚弄,但它往往会谨慎行事(通常并非总是如此)。

作为未来访问者的参考,这些是我看到编译器非常困惑的场景(我假设所有这些情况都是设计使然):

空原
  • 谅空。通常用于避免取消引用警告,但使对象不可为空。看起来想把你的脚放在两只鞋里。
string s = null!; //No warning

  • 表面分析。与 ReSharper(使用代码注释执行此操作)相反,C# 编译器仍然不支持处理可为空引用的完整属性范围。
void DoSomethingWith(string? s)
{    
ThrowIfNull(s);
var split = s.Split(' '); //Dereference warning
}

但是,它确实允许使用一些构造来检查可空性,这也消除了警告:

public static void DoSomethingWith(string? s)
{
Debug.Assert(s != null, nameof(s) + " != null");
var split = s.Split(' ');  //No warning
}

或者(仍然很酷)属性(在这里找到它们):

public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
{
...
}

  • 易受影响的代码分析。这就是你揭露的。编译器必须做出假设才能工作,有时它们可能看起来违反直觉(至少对人类而言)。
void DoSomethingWith(string s)
{    
var b = s == null;
var i = s.Length; // Dereference warning
}

  • 泛型的麻烦。在这里问,在这里解释得很好(和以前一样的文章,段落"T的问题?泛型很复杂,因为它们必须使引用和值都满意。主要区别在于,虽然string?只是一个字符串,但int?成为一个Nullable<int>,并强制编译器以完全不同的方式处理它们。同样在这里,编译器选择安全路径,强制您指定预期的内容:
public interface IResult<out T> : IResult
{
T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
}

解决了给予约束:

public interface IResult<out T> : IResult where T : class { T? Data { get; }}
public interface IResult<T> : IResult where T : struct { T? Data { get; }}

但是,如果我们不使用约束并从数据中删除"?",我们仍然可以使用"default"关键字在其中放置 null 值:

[Pure]
public static Result<T> Failure(string description, T data = default)
=> new Result<T>(ResultOutcome.Failure, data, description); 
// data here is definitely null. No warning though.

最后一个对我来说似乎更棘手,因为它确实允许编写不安全的代码。

希望这对某人有所帮助。

相关内容

最新更新