Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Ternary Operator evaluating when it shouldn't [closed]

This piece of code caught me out today:

clientFile.ReviewMonth == null ? null : MonthNames.AllValues[clientFile.ReviewMonth.Value]

clientFile.Review month is a byte? and its value, in the failing case, is null. The expected result type is string.

The exception is in this code

    public static implicit operator string(LookupCode<T> code)
    {
        if (code != null) return code.Description;

        throw new InvalidOperationException();
    }

The right hand side of the evaluation is being evaluated and then implicitly converted to a string.

But my question is why is the right hand side being evaluated at all when clearly only the left hand side should be evaluated? (The documentation states that "Only one of the two expressions is evaluated.")

The solution incidentally is to cast the null to string - that works but Resharper tells me that the cast is redundant (and I agree)

Edit: This is different to the "Why do I need to add a cast before it will compile" type ternary operator question. The point here is that no cast is required to make it compile - only to make it work correctly.

like image 626
Simon Hewitt Avatar asked Nov 14 '16 07:11

Simon Hewitt


1 Answers

You're forgetting that implicit operators are determined at compile-time. That means that the null you have is actually of type LookupCode<T> (due to the way type inference works in a ternary operator), and needs to be cast using the implicit operator to a string; that's what gives you your exception.

void Main()
{
  byte? reviewMonth = null;

  string result = reviewMonth == null 
                  ? null // Exception here, though it's not easy to tell
                  : new LookupCode<object> { Description = "Hi!" };

  result.Dump();
}

class LookupCode<T>
{
  public string Description { get; set; }

  public static implicit operator string(LookupCode<T> code)
  {
      if (code != null) return code.Description;

      throw new InvalidOperationException();
  }
}

The invalid operation doesn't happen on the third operand, it happens on the second - the null (actually a default(LookupCode<object>)) isn't of type string, so the implicit operator is invoked. The implicit operator throws an invalid operation exception.

You can easily see this is true if you use a slightly modified piece of code:

string result = reviewMonth == null 
                ? default(LookupCode<object>) 
                : "Does this get evaluated?".Dump();

You still get an invalid operation exception, and the third operand isn't evaluated. This is of course painfully obvious in the generated IL: the two operands are two separate branches; there's no way for them both to be executed. And the first branch has another painfully obvious thing:

ldnull      
call        LookupCode`1.op_Implicit

It's not even hidden anywhere :)

The solution is simple: use an explicitly typed null, default(string). R# is simply wrong - (string)null isn't the same as null in this case, and R# has wrong type inference in this scenario.

Of course, this is all described in the C# specification (14.13 - Conditional operator):

The second and third operands of the ?: operator control the type of the conditional expression.

Let X and Y be the types of the second and third operands. Then,

  • If X and Y are the same type, then this is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§13.1) exists from X to Y, but not from Y to X, then Y is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§13.1) exists from Y to X, but not from X to Y, then X is the type of the conditional expression.
  • Otherwise, no expression type can be determined, and a compile-time error occurs.

In your case, an implicit conversion exists from LookupCode<T> to string, but not vice versa, so the type LookupCode<T> is preferred to string. The interesting bit is that since this is all done at compile-time, the LHS of the assignment actually makes a difference:

string result = ... // Fails
var result = ... // Works fine, var is of type LookupCode<object>
like image 134
Luaan Avatar answered Nov 13 '22 18:11

Luaan