Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a method like this, which is a simplified version of some logic in the production code:

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

I get a "Dereference of a possibly null reference" error when accessing the y.Length.

If I change & to &&, the warning goes away. I couldn't figured it out why at first glance, then I thought it may be because the & operator is overloadable, so the behaviour might change and possibly it could evaluate to true even when the y is null. Is my assumption correct or am I missing something else?

Note: I can reproduce this in an app that targets .NET Core 3.1 or .NET Standard 2.0 using Visual Studio 2019.

like image 500
Selman Genç Avatar asked Nov 06 '22 08:11

Selman Genç


1 Answers

There are a few reasons the compiler doesn't recognize this pattern. The most obvious is just engineering effort. Using &/| for boolean logic is just seen as less common than &&/|| and therefore other things were prioritized over full support for &/| in flow analysis.

The other reason is that there would actually be a complexity cost to supporting this in nullable analysis. Consider the following admittedly contrived example: 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;
    }
}
  • After visiting the expression x != null, the state of x is "not null when true", and "maybe null when false".
  • When we visit func1(z = y = x), we have to assume the worst case--x may be null, since we get there regardless of whether x != null was true or false. Because of this we have to give a warning on assignment to non-nullable out parameter z.
  • Then, after we finish visiting the & operator, we have to assume that if the result was true, then all the operands returned true, and otherwise any of the operands could have returned false.
  • But hold on! Now if we have to assume that the operands all returned true, that means x was really not-null all along, and because x was assigned to y, that y was really not-null all along. The only practical way to account for this in this particular kind of analysis is to visit func1(z = y = x) a second time, but with an initial state where x != null was true.

In other words, visiting a & operator with full handling of nullable conditional states requires visiting the right-hand side twice--once assuming the worst-case result from the left side to produce diagnostics for it, and again assuming the left side was true, so that we can produce a final state for the operator. In contrived examples this effect can be compounding, such as x != null & (y != null & z != null), where we end up having to visit z != null 4 times.

In order to sidestep this complexity, we've opted to not make nullable analysis work with &/| at this time. The short-circuiting behavior of &&/|| means they don't suffer from the problems described above, so it's actually simpler to make them work correctly.

like image 76
Rikki Gibson Avatar answered Nov 15 '22 05:11

Rikki Gibson