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

And*_*ykh 19 c# roslyn c#-8.0 nullable-reference-types

在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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

我没有创建一个新的 GitHub 问题,也没有占用非常忙碌的项目贡献者的时间,而是将其发布给社区。

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

小智 11

这实际上是@stuartd 链接的答案的重复,所以我不打算在这里详细介绍。但问题的根源在于,这既不是语言错误也不是编译器错误,而是完全按照实现的预期行为。我们跟踪变量的空状态。当您最初声明变量时,该状态为 NotNull,因为您使用非空值显式初始化它。但是我们不跟踪 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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

它不会变为可空,仅仅是因为我们愚蠢到将它与null.

是的,确实如此。给编译器。作为人类,我们可以看看这段代码,很明显地明白它不能抛出空引用异常。但是在编译器中实现可空流分析的方式,它不能。我们确实讨论了对该分析的一些改进,其中我们根据值的来源添加了额外的状态,但我们认为这为实现增加了大量的复杂性,而不会带来很大的收益,因为唯一的地方这对于这样的情况很有用,在这种情况下,用户使用 anew或常量值初始化变量,然后null无论如何都要检查它。


Alv*_*tor 9

如果可空性推断不是万无一失的,[..]它怎么会出错呢?

C#8 的可为空引用一可用,我就很高兴地采用了它们。由于我习惯使用 ReSharper 的 [NotNull](等)表示法,我确实注意到两者之间存在一些差异。

C# 编译器可能会被愚弄,但它往往会因为谨慎而犯错误(通常,但并非总是如此)。

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

  • null 宽容 null。通常用于避免取消引用警告,但保持对象不可为空。看起来就像想让你的脚穿两只鞋一样。
    string s = null!; //No warning
Run Code Online (Sandbox Code Playgroud)
  • 表面分析。与 ReSharper(使用代码注释来实现)相反,C# 编译器仍然不支持完整范围的属性来处理可为空引用。
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }
Run Code Online (Sandbox Code Playgroud)

不过,它确实允许使用某种构造来检查可空性,这也可以消除警告:

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

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

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }
Run Code Online (Sandbox Code Playgroud)
  • 易受影响的代码分析。这就是你揭露的情况。编译器必须做出假设才能工作,有时它们可​​能看起来违反直觉(至少对于人类来说)。
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }
Run Code Online (Sandbox Code Playgroud)
  • 泛型的麻烦在这里问并在这里解释得很好(与之前的同一篇文章,“T 的问题?”段落)。泛型很复杂,因为它们必须使引用和值都满意。主要区别在于 whilestring?只是一个字符串,int?变成了 aNullable<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.
    }
Run Code Online (Sandbox Code Playgroud)

解决给予限制:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}
Run Code Online (Sandbox Code Playgroud)

但如果我们不使用约束并删除“?” 从数据中,我们仍然可以使用“default”关键字将空值放入其中:

    [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.
Run Code Online (Sandbox Code Playgroud)

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

希望这对某人有帮助。