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.