当似乎无法实现空引用时,为什么会收到可能的空引用空引用警告?


9

阅读了有关HNQ的问题之后,我继续阅读了有关C#8中的Nullable引用类型的信息,并进行了一些实验。

我非常清楚,有人说“我发现了编译器错误!”,十分之九,甚至更多。这实际上是设计使然,并对自己产生误解。而且,因为我直到今天才开始研究此功能,所以显然我对它没有很好的了解。这样处理后,让我们看下面的代码:

#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在线生成警告,为什么CS8602在这里似乎没有null参考的情况下我们会有 警告?如果可空性推断不是防弹的,如链接的GitHub线程所暗示的,它怎么会出错?那有什么例子呢?


似乎编译器本身设置的行为是,此时变量“ s”可能为空。无论如何,如果我曾经使用过字符串或对象,那么在调用函数之前应该始终进行检查。“ s?.Length”应该可以解决问题,警告本身也应该消失。
chg

1
@chg,?因为s它不能为空,所以不需要。它不会变为可为空,仅仅是因为我们足够愚蠢以将其与进行比较null
Andrew Savinykh

我关注的是一个较早的问题(对不起,找不到它)在哪里,如果您添加检查值是否为null的情况,则编译器会将其视为“提示”,表明该值可能为null,即使事实并非如此。
斯图尔特(Stuartd)

@stuartd,是的,看起来就是这样。所以现在的问题是:为什么这有用?
Andrew Savinykh

1
@chg,好吧,这就是我在问题正文中所说的,不是吗?
Andrew Savinykh

Answers:


7

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

是的,实际上是这样。给编译器。作为人类,我们可以看一下这段代码,显然可以理解它不能引发空引用异常。但是在编译器中实现可为空的流分析的方式却不能。我们确实讨论了此分析的一些改进,其中我们根据值的来源添加了其他状态,但是我们决定这样做会给实现增加很多复杂性,而不会带来很多好处,因为唯一的地方这对于这样的情况很有用,在这种情况下,用户使用a new或常数值初始化变量,然后null无论如何都要检查它。


谢谢。这主要解决与另一个问题的相似性,我想进一步解决差异。例如,为什么s == null不产生警告?
Andrew Savinykh

我也意识到了#nullable enable; string s = "";s = null;编译并工作(仍然会产生警告),实现在启用的null注释上下文中将null分配给“ non-nullable引用”的实现有什么好处?
安德鲁Savinykh

Mad的答案集中于以下事实:“ [编译器]不会跟踪单独变量的状态之间的关系”,在此示例中我们没有单独的变量,因此在将Mad的其余答案应用于这种情况时,我遇到了麻烦。
Andrew Savinykh

不用说,但是请记住,我在这里不是批评,而是学习。从C#于2001年问世以来,我就一直在使用它。尽管我对这门语言并不陌生,但编译器的行为方式令我感到惊讶。这个问题的目的是为了弄清为什么这种行为对人类有用的理由。
Andrew Savinykh

有正当的理由检查s == null。例如,也许您处于公共方法中,并且想要进行参数验证。或者,也许您使用的库注释不正确,直到他们修复了该错误,您必须在未声明的地方处理null。无论哪种情况,如果我们警告,那将是糟糕的经历。至于允许赋值:局部变量注释仅供阅读。它们根本不影响运行时。实际上,我们将所有这些警告都放在一个错误代码中,以便在您希望减少代码流失时将其关闭。
333fred

0

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

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

C#编译器可能会被愚弄,但在谨慎方面(通常,并非总是如此)往往会犯错

作为将来的参考,以下是我看到编译器非常困惑的情况(我假设所有这些情况都是设计使然):

  • Null宽恕null。通常用于避免取消引用警告,但保持对象不可为空。好像想要把脚放在两双鞋里。
    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?变成a 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; }}

但是,如果我们不使用约束并删除“?” 在Data中,我们仍然可以使用'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.

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

希望这对某人有帮助。


我建议给可空属性的文档阅读:docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes。他们将解决您的一些问题,尤其是在表面分析部分和泛型方面。
333fred

感谢您的链接@ 333fred。即使有一些属性可以使用,但解决我发布的问题的属性(告诉我ThrowIfNull(s);确保我s不为null的东西)并不存在。本文还解释了如何处理不可为空的泛型,而我向您展示了如何“愚弄”编译器,该编译器具有null值,但没有警告。
Alvin Sartor

实际上,该属性确实存在。我在文档上提交了一个错误以添加它。您正在寻找DoesNotReturnIf(bool)
333fred

@ 333fred实际上我正在寻找类似的东西DoesNotReturnIfNull(nullable)
Alvin Sartor
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.