使用 C#8.0 优化 Visual Studio 中的 switch case 后出现意外结果

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

今天在编码时,visual studio 通知我可以优化我的 switch case。但是我拥有的代码与 visual studio 从我的 switch case 生成的代码不会产生相同的结果。

我使用的枚举:

public enum State
{
    ExampleA,
    ExampleB,
    ExampleC
};

在下面的代码 运行s 之后,值等于 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;
}

但是当visual studio优化switch case时,这个值变成了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()
};

这只是包含重现错误输出的信息的代码,而不是 运行 生产中的实际代码。我发现奇怪的是,如果我在最后一个代码块中注释掉 State.ExampleA 行,则会写入正确的值。

我的问题是:这是一个错误吗?或者我在这里遗漏了什么?

这会起作用:

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 所以编译器 infers float (介于 floatuintshort) 作为 switch 表达式的输出类型(并将 uintshort 转换为它),然后将其结果转换为 double,这会导致 ExampleB 情况下的精度损失。

这突出了语句表达式之间的区别。你之前的开关是一个开关语句,这是得到运行.

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

此处您正在将 uint(右侧)转换为 double(左侧)。实际上,您在 switch 语句的每个分支中都进行了不同的转换,这很好,因为它们是单独的赋值语句。

将其与优化后所做的进行比较:switch 语句变成了 switch expressionexpressions 有一个 type。这个表达式的类型是什么?

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()
}

开关的每个分支 returns 不同的类型 - 分别为 floatuintshort因此 C# 需要找到一种所有这三种类型都可以隐式转换的类型。并找到 float。 C# 不能仅仅“找出” 运行 时的开关 returns 并计算出“动态”执行的转换。

每个分支返回的东西都要先转换成一个float。因此,整个表达式的类型是float。最后,您将 float 分配给 value,这是一个 double.

所以整体转换为uint -> float -> double,造成精度损失

可能和其他人一样,我也想到了这个问题,因为我真的对 switch 表达式的这些语义感到惊讶。这是一个类似于我最初的例子:

{
    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());
}

现在,我的 IDE (JetBrains Rider) 建议我可以将其转换为 switch 表达式。最终结果是这样的:

{
    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());
}

此问题的根本原因与此问题中发布的相同。表达式只有一种类型,doubleint 都可以隐式转换为 double(但 不能 转换为 int! ) => 这是 C# 编译器选择的表达式的目标类型。


底线:使用 switch 表达式时要非常小心,尤其是在我的用例中使用 object 等松散类型的变量时。在这种情况下写出变量的类型可能更好(也更安全),使 reader 更明显(嘿,那是 you 只是一些days/weeks/months从现在开始!)代码如何工作。