Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the 2nd term in this If-AndAlso statement evaluated early?

Tags:

vb.net

Edit for clarity: This is a pretty odd behaviour from the compiler, I'm asking for why it behaves this way in general, rather than how to work around it (there are several simple solutions already).

Recently, I came across a piece of code which throws contains a subtle mistake, and ends up throwing an exception. A shortened, contrived example:

Dim list As List(Of Integer) = Nothing
If list?.Any() AndAlso list.First() = 123 Then
    MessageBox.Show("OK then!")
End If

In the real example, list was only occasionally Nothing, I'm just shortening the code for clarity. The intention with the first term in the If statement was to both check that list is not Nothing, and to also test for the existence of at least one element. Since in this case, list is Nothing, the list?.Any() actually / typically evaluates to Nothing. Somewhat counter-intuitively, the 2nd term, namely list.First() = 123 is also evaluated by the runtime, causing an obvious exception. This is somewhat counter-intuitive, since at first guess most people would imagine that Nothing is seem as False, and since we're using an AndAlso here, the short-circuit operator would prevent the 2nd half of the If statement from executing.

Additional investigation / "What have you tried:"

Quick check to confirm that a shortened If list?.Any() Then seems to treat list?.Any() as a False:

Dim list As List(Of Integer) = Nothing

If list?.Any() Then
    MessageBox.Show("OK then!")   'This line doesn't get hit / run
End If

Also, we can work around the issue by in several ways: If list IsNot Nothing AndAlso list.Any() AndAlso list.First() = 123 Then would work just fine, as would If If(list?.Any(), False) AndAlso list.First() = 123 Then.

Since VB.Net is not my usual language, I thought I'd have a look at this in C#:

List<int> list = null;
if (list?.Any() && list.First() == 123)
{
    MessageBox.Show("OK then!");
}

However, this gives a compilation error:

error CS0019: Operator '&&' cannot be applied to operands of type 'bool?' and 'bool'

Apart from the obvious fact that the stricter compiler check would prevent this mistake from being made in the C# scenario, this leads me to believe that type coercion is happening in the VB.Net scenario. One guess might be that the compiler is trying to cast the Boolean result of the 2nd term to a nullable Boolean, however this doesn't make a whole lot of sense to me. Specifically, why would it evaluate it prior to / same time as the left side, and abandoning the entire process early, like it should? Looking back at the VB.Net examples that do work correctly, all involve explicit checks which have a simple Boolean result, rather than a nullable Boolean.

My hope is that someone can give some good insight into this behaviour!

like image 716
Daniel B Avatar asked Nov 16 '22 19:11

Daniel B


1 Answers

This appears to be an omission(bug) in the VB compiler's syntax evaluation. The documentation for the ?. and ?() null-conditional operators (Visual Basic) states:

Tests the value of the left-hand operand for null (Nothing) before performing a member access (?.) or index (?()) operation; returns Nothing if the left-hand operand evaluates to Nothing. Note that, in the expressions that would ordinarily return value types, the null-conditional operator returns a Nullable.

The expression list?.Any() (Enumerable.Any Method) would ordinarily return a Boolean (a ValueType), so we should expect list?.Any() to yield a Nullable(Of Boolean).

We should see a compiler error as a Nullable(Of Boolean) can not participate in an AndAlso Operator expression.

Interestingly, if we treat list?.Any() as a Nullable(Of Boolean), it is seen as documented.

If (list?.Any()).HasValue() AndAlso list.First = 123 Then
 ' something
End If

Edit: The above does not really address your why?.

If you de-compile the generated IL, you get something like this:

 Dim source As List(Of Integer) = Nothing
 Dim nullable As Boolean?
 Dim nullable2 As Boolean? = nullable = If((Not source Is Nothing), New Boolean?(Enumerable.Any(Of Integer)(source)), Nothing)
 nullable = If((nullable2.HasValue AndAlso Not nullable.GetValueOrDefault), False, If((Enumerable.First(Of Integer)(source) Is &H7B), nullable, False))
 If nullable.GetValueOrDefault Then
    MessageBox.Show("OK then!")
 End If

This obviously will will not compile, but if we clean it up a bit, the source of the issue becomes apparent.

Dim list As List(Of Integer) = Nothing
Dim nullable As Boolean?
Dim nullable2 As Boolean? = If(list IsNot Nothing, 
        New Boolean?(Enumerable.Any(Of Integer)(list)),
        Nothing)

' nullable2 is nothing, so the 3rd line below is executed and throws the NRE

nullable = If((nullable2.HasValue AndAlso Not nullable.GetValueOrDefault),
        False,
        If((Enumerable.First(Of Integer)(list) = 123), nullable, False))

If nullable.GetValueOrDefault Then
    MessageBox.Show("OK then!")
End If

Edit2:

The OP has found the following statement from the documentation for Nullable Value Types (Visual Basic)

AndAlso and OrElse, which use short-circuit evaluation, must evaluate their second operands when the first evaluates to Nothing.

This statement makes sense if Option Strict Off is in force and using the OrElse Operator as Nothing can implicitly be converted to False. For the OrElse operator, the second expression is not evaluated only if the first expression is True. In the case of the AndAlso operator, the second operator is not evaluated if the first expression is True.

Also, consider the following code snippet with Option Strict On.

Dim list As List(Of Integer) = Nothing
Dim booleanNullable As Nullable(Of Boolean) = list?.Any()
Dim b As Boolean = (booleanNullable AndAlso list.First() = 123)
If b Then
    ' do something
End If

This re-arrangement of the original logic does yield a compiler error. compiler error

With Option Strict Off, no compiler error is generated, yet the same run-time error occurs.

My Conclusion: As originally stated, this is a bug. When an AndAlso operator is included in a If-Then block the compiler treats the result of the null conditional operator using Option Strict Off type conversion relaxation regardless of the actual state of Option Strict.

like image 146
TnTinMn Avatar answered Nov 19 '22 08:11

TnTinMn