Unexpected results after optimizing switch case in Visual Studio with C#8.0

Viewed 758

Today while coding, visual studio notified me that my switch case could be optimized. But the code that I had vs the code that visual studio generated from my switch case does not result in the same outcome.

The Enum I Used:

public enum State
{
    ExampleA,
    ExampleB,
    ExampleC
};

After the following code runs the value is equal to 2147483647.

State stateExample = State.ExampleB;
double value;

switch (stateExample)
{
    case State.ExampleA:
        value = BitConverter.ToSingle(BitConverter.GetBytes((long)2147483646), 0);
        break;
    case State.ExampleB:
        value = BitConverter.ToUInt32(BitConverter.GetBytes((long)2147483647), 0);
        break;
    case State.ExampleC:
        value = BitConverter.ToInt16(BitConverter.GetBytes((long)2147483648), 0);
        break;
    default:
        value = 0;
        break;
}

But when visual studio optimized the switch case, the value becomes 2147483648.

State stateExample = State.ExampleB;
double value = stateExample switch
{
    State.ExampleA => BitConverter.ToSingle(BitConverter.GetBytes((long)2147483646), 0), //Commenting this line results in correct value
    State.ExampleB => BitConverter.ToUInt32(BitConverter.GetBytes((long)2147483647), 0),
    State.ExampleC => BitConverter.ToInt16(BitConverter.GetBytes((long)2147483648), 0),
    _ => throw new InvalidOperationException()
};

This is just the code with information that reproduced the erroneous output and not actual code that is run in production. What I found weird was that if I comment out the line State.ExampleA in the last code block the correct value is written.

My Question is: Is this a bug? Or am I missing something here?

3 Answers

This highlights the difference between a statement and an expression. The switch you had before, was a switch statement and this was the assignment that got run.

value = BitConverter.ToUInt32(BitConverter.GetBytes((long)2147483647), 0);

Here you are converting an uint (right hand side) to a double (left hand side). You were actually doing a different conversion in each branch of your switch statement, and this was fine, because well, they are separate assignment statements.

Compare that to what you are doing after the optimisation: the switch statement became a switch expression. And expressions have a single type. What is type of this expression?

stateExample switch
{
    State.ExampleA => BitConverter.ToSingle(BitConverter.GetBytes((long)2147483646), 0), //Commenting this line results in correct value
    State.ExampleB => BitConverter.ToUInt32(BitConverter.GetBytes((long)2147483647), 0),
    State.ExampleC => BitConverter.ToInt16(BitConverter.GetBytes((long)2147483648), 0),
    _ => throw new InvalidOperationException()
}

Each branch of the switch returns a different type - float, uint and short respectively. So C# needs to find a type to which all three of these can be implicitly converted. And float is found. C# can't just "figure out" what the switch returns at runtime and work out the conversion to perform "dynamically".

The things returned in each branch has to be first converted to a float. Therefore, the type of the whole expression is float. Finally, you assign the float to value, which is a double.

So the overall conversion is uint -> float -> double, causing a loss of precision.

This will work:

double value1 = stateExample switch
    {
        State.ExampleA => (double)BitConverter.ToSingle(BitConverter.GetBytes((long)2147483646), 0), //Commenting this line results in correct value
        State.ExampleB => BitConverter.ToUInt32(BitConverter.GetBytes((long)2147483647), 0),
        State.ExampleC => BitConverter.ToInt16(BitConverter.GetBytes((long)2147483648), 0),
        _ => throw new InvalidOperationException()
    };

BitConverter.ToSingle returns float so compiler infers float (between float, uint and short) as output type for the switch expression (and casting uint and short to it) and then casts it's result to double which results in precision loss for ExampleB case.

Like probably others, I also came to this question because I really got surprised by these semantics of switch expressions. Here's an example similar to what I had at first:

{
    object left = 1;
    object value;

    switch (left)
    {
        case int previousInt: // <-- this case gets used
            value = previousInt + 1;
            break;
        case double previousDouble:
            value = previousDouble + 2;
            break;
        default:
            throw new InvalidOperationException();
    }

    // Output:
    // 2
    // System.Int32 (expected)
    Console.WriteLine(value);
    Console.WriteLine(value.GetType());
}

Now, my IDE (JetBrains Rider) suggested that I could convert this to a switch expression. The end result is something like this:

{
    object left = 1;

    object value = left switch
    {
        int previousInt => previousInt + 1, // <-- this "case" gets used
        double previousDouble => previousDouble + 2,
        _ => throw new InvalidOperationException()
    };

    // Output:
    // 2
    // System.Double (NOT what I expected)
    Console.WriteLine(value);
    Console.WriteLine(value.GetType());
}

The root cause of this is the same as posted in this question. An expression has a single type, and double and int can both be implicitly converted to double (but not to int!) => this is the target type of the expression chosen by the C# compiler.


Bottom line: Be very careful when using switch expressions, especially when using loosely typed variables like object in my use case. It's probably much better (and safer) to write out the type of the variable in this case, to make it more obvious to the reader (hey, that's you just a few days/weeks/months from now on!) how the code works.

Related