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

Posted

技术标签:

【中文标题】当空引用似乎不可能时,为啥我们会收到可能的取消引用空引用警告?【英文标题】:Why do we get possible dereference null reference warning, when null reference does not seem to be possible?当空引用似乎不可能时,为什么我们会收到可能的取消引用空引用警告? 【发布时间】:2020-04-18 19:55:05 【问题描述】:

在阅读了 HNQ 上的 this question 之后,我继续阅读了有关 Nullable Reference Types in 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 进行比较是没有意义的。

相反,我在 next 行收到警告,警告说 s 可能是空引用,尽管对于人类来说,显然不是。

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

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

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

能否请您解释一下发生了什么以及为什么?特别是,为什么在s == null 行上没有生成警告,为什么我们有CS8602,而null 在这里似乎不可能引用?如果可空性推断不是万无一失的,正如链接的 GitHub 线程所暗示的那样,它怎么会出错?有什么例子?

【问题讨论】:

似乎编译器本身设置了此时变量“s”可能为空的行为。无论如何,如果我曾经使用过字符串或对象,那么在调用函数之前应该始终进行检查。 "s?.Length" 应该可以解决问题,并且警告本身应该消失。 @chg,应该不需要?,因为s 不能为空。它不会变成可以为空的,仅仅是因为我们愚蠢到将它与null 进行比较。 我在关注一个较早的问题(抱歉,找不到它),其中假设如果您添加一个值是否为空的检查,那么编译器会将其视为“提示”值可能为空,即使事实证明并非如此。 @stuartd,是的,看起来就是这样。所以现在的问题是:为什么这有用? @chg,好吧,这就是我在问题正文中所说的,不是吗? 【参考方案1】:

这实际上是@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
    

在这两种情况下,您都明确地测试 s 是否为 null。我们将此作为流量分析的输入,正如 Mads 在这个问题中回答的那样:https://***.com/a/59328672/2672518。在该答案中,结果是您在退货时收到警告。在这种情况下,答案是您会收到一条警告,指出您取消引用了可能为空的引用。

它不会变成可以为空的,仅仅是因为我们愚蠢到将它与null进行比较。

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

【讨论】:

谢谢。这主要解决了与其他问题的相似之处,我想更多地解决差异。例如,为什么s == null 不产生警告? 我也刚刚意识到#nullable enable; string s = "";s = null; 编译并工作(它仍然会产生警告)允许在启用的 null 注释上下文中将 null 分配给“不可为 null 的引用”的实现有什么好处? Mad 的答案集中在“[编译器] 不跟踪单独变量的状态之间的关系”这一事实上,我们在这个例子中没有单独的变量,所以我在应用 Mad 的其余部分时遇到了麻烦回答这个案例。 不言而喻,但请记住,我来这里不是为了批评,而是为了学习。从 2001 年 C# 首次问世起,我就一直在使用它。尽管我对这门语言并不陌生,但编译器的行为方式让我感到惊讶。这个问题的目的是解释为什么这种行为对人类有用。 有正当理由检查s == null。例如,也许您在一个公共方法中,并且想要进行参数验证。或者,也许您使用的库注释不正确,并且在他们修复该错误之前,您必须处理未声明的 null 。在任何一种情况下,如果我们发出警告,那将是一次糟糕的经历。至于允许赋值:局部变量注释仅供阅读。它们根本不影响运行时。事实上,我们将所有这些警告放在一个错误代码中,以便您可以在想要减少代码流失时将其关闭。【参考方案2】:

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

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

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

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

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

表面分析。与 ReSharper(使用 code annotation)相反,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
    

或者(还是很酷的)属性(全部找到here):

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

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

泛型问题。询问here 并很好地解释了here(与之前相同的文章,段落“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; 

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

    [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.

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

希望这对某人有所帮助。

【讨论】:

我建议阅读有关可空属性的文档:docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes。他们将解决您的一些问题,尤其是表面分析部分和泛型。 感谢@333fred 的链接。即使您可以使用某些属性,但解决我发布的问题的属性(告诉我ThrowIfNull(s); 向我保证s 不为空)并不存在。这篇文章还解释了如何处理 non-nullable 泛型,而我展示了如何“欺骗”编译器,获得一个 null 值但没有警告。 其实属性确实存在。我在文档上提交了一个错误以添加它。你正在寻找DoesNotReturnIf(bool) @333fred 实际上我正在寻找更像DoesNotReturnIfNull(nullable) 的东西。 “希望这对某人有所帮助”——确实如此;)也来自 Resharper 的属性。我还必须更改一些代码,因为 C# 8 属性还不能涵盖所有情况(最简单的例子是,虽然有 NotNullIfNotNull,但没有 NullIfNotNull,或它的其他变体)。

以上是关于当空引用似乎不可能时,为啥我们会收到可能的取消引用空引用警告?的主要内容,如果未能解决你的问题,请参考以下文章

取消引用结构会返回结构的新副本吗?

为啥 INVOKE 总是取消引用数据成员而不是尽可能地调用?

为啥不可能有一个对 void 的引用?

C++:取消引用指针

为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?

正确使用range(),为啥pylint会抱怨:不迭代时引用了range built-in