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.