为什么我在使用非短路 AND 运算符时会收到可能为空引用的取消引用警告?

Why do I get dereference of a possibly null reference warning when using non-short-circuit AND operator?

我有一个这样的方法,它是生产代码中一些逻辑的简化版本:

static void Foo(int a, int b, string x, string y)
{
     if (a > 100 && b < 50 && (x != null) & (y != null))
     {
          Console.WriteLine(y.Length);
     }
}

访问 y.Length 时出现 "Dereference of a possibly null reference" 错误。

如果我将 & 更改为 &&,警告就会消失。乍一看我不明白为什么,然后我认为这可能是因为 & 运算符是可重载的,所以行为可能会改变并且可能计算为 true 即使 y 为空。我的假设是正确的还是我遗漏了什么?

注意:我可以使用 Visual Studio 2019 在面向 .NET Core 3.1 或 .NET Standard 2.0 的应用程序中重现此内容。

编译器无法识别此模式的原因有几个。最明显的只是工程上的努力。将 &/| 用于布尔逻辑被认为不如 &&/|| 常见,因此其他事情优先于完全支持 &/|在流量分析中。

另一个原因是,在可空分析中支持这一点实际上会产生复杂性成本。考虑以下公认的人为示例:SharpLab

func(null, null, out _);

void func(C? x, C? y, out C z)
{
    if (x != null & func1(z = y = x)) // should warn on 'y = x'
    {
        x.ToString(); // warns, but perhaps shouldn't
        y.ToString(); // warns, but perhaps shouldn't
    }
}

bool func1(object? obj) => true;

class C
{
    public C Inner { get; set; }
    
    public C()
    {
        Inner = this;
    }
}
  • 访问表达式x != null后,x的状态为“为真时不为空”,“为假时可能为空”。
  • 当我们访问 func1(z = y = x) 时,我们必须假设最坏的情况--x 可能为空,因为无论 x != null 是真还是假,我们都会到达那里。因此,我们必须在分配给不可为空的输出参数 z.
  • 时发出警告
  • 然后,在我们访问完 & 运算符后,我们必须假设如果结果为真,则所有操作数都返回真,否则任何操作数都可能返回假。
  • 但是坚持住!现在,如果我们必须假设所有操作数都返回 true,那意味着 x 一直都是非空的,并且因为 x 被分配给 y,所以 y一直都不是空的。在这种特殊类型的分析中,唯一可行的方法是访问 func1(z = y = x) 第二次 ,但初始状态 x != null 为真。

换句话说,访问完全处理可为空条件状态的 & 运算符需要访问右侧两次——一次假设左侧的最坏情况结果为其生成诊断,并再次假设左侧为真,这样我们就可以为运算符生成最终状态。在人为设计的示例中,这种效果可能是复合的,例如 x != null & (y != null & z != null),我们最终不得不访问 z != null 4 次。

为了避免这种复杂性,我们选择此时不对 &/| 进行可空分析。 &&/|| 的短路行为意味着它们不会遇到上述问题,因此使它们正常工作实际上更简单。