从int中提升/可空转换的严重错误,允许从十进制转换

Jep*_*sen 59 .net c# nullable compiler-bug implicit-conversion

我认为这个问题会让我在Stack Overflow上立刻成名.

假设您有以下类型:

// represents a decimal number with at most two decimal places after the period
struct NumberFixedPoint2
{
    decimal number;

    // an integer has no fractional part; can convert to this type
    public static implicit operator NumberFixedPoint2(int integer)
    {
        return new NumberFixedPoint2 { number = integer };
    }

    // this type is a decimal number; can convert to System.Decimal
    public static implicit operator decimal(NumberFixedPoint2 nfp2)
    {
        return nfp2.number;
    }

    /* will add more nice members later */
}
Run Code Online (Sandbox Code Playgroud)

它的编写使得只允许安全转换不会丢失精度.但是,当我尝试这段代码时:

    static void Main()
    {
        decimal bad = 2.718281828m;
        NumberFixedPoint2 badNfp2 = (NumberFixedPoint2)bad;
        Console.WriteLine(badNfp2);
    }
Run Code Online (Sandbox Code Playgroud)

我很惊讶这个编译,并在运行时,写出来2.从int(值2)转换到NumberFixedPoint2这里很重要.(过载WriteLine,取入一个System.Decimal是优选的,在任何人的情况下奇观.)

为什么地球上的转换decimalNumberFixedPoint2允许的?(顺便说一下,在上面的代码中,如果NumberFixedPoint2从结构变为类,则没有任何变化.)

您是否知道C#语言规范是否表示从int自定义类型的隐式转换"暗示"是否存在"直接"显式转换decimal为该自定义类型?

它变得更糟.请尝试使用此代码:

    static void Main()
    {
        decimal? moreBad = 7.3890560989m;
        NumberFixedPoint2? moreBadNfp2 = (NumberFixedPoint2?)moreBad;
        Console.WriteLine(moreBadNfp2.Value);
    }
Run Code Online (Sandbox Code Playgroud)

如您所见,我们Nullable<>在这里进行了(解除)转换.但是哦,是的,那确实可以编译.

x86 "platform"中编译时,此代码会写出不可预测的数值.哪一个不时变化.举个例子,有一次我得到了2289956.现在,这是一个严重的错误!

当为x64平台编译时,上面的代码使用System.InvalidProgramException带有消息的公共语言运行时检测到无效程序而使应用程序崩溃.根据InvalidProgramException班级文件:

通常,这表示编译器中生成程序的错误.

有没有人(比如Eric Lippert,或曾经在C#编译器中解除过转换的人)知道这些错误的原因?比如,我们的代码中没有遇到它们的充分条件是什么?因为这种类型NumberFixedPoint2实际上是我们在实际代码中所拥有的东西(管理其他人的钱和东西).

Jon*_*eet 44

我只是回答问题的第一部分.(我建议第二部分应该是一个单独的问题;它更可能是一个错误.)

这里只有一个明确的从转换decimalint,但是转换被隐式调用你的代码.转换发生在此IL中:

IL_0010:  stloc.0
IL_0011:  ldloc.0
IL_0012:  call       int32 [mscorlib]System.Decimal::op_Explicit(valuetype [mscorlib]System.Decimal)
IL_0017:  call       valuetype NumberFixedPoint2 NumberFixedPoint2::op_Implicit(int32)
Run Code Online (Sandbox Code Playgroud)

我相信这是符合规范的正确行为,即使它令人惊讶1.让我们按照C#4规范(用户定义的显式转换)的第6.4.5节进行操作.我不打算复制所有文本,因为这将是乏味的 - 只是在我们的案例中相关结果.同样我不打算使用下标,因为它们在这里与代码字体不兼容:)

  • 确定类型S0T0:S0decimal,T0NumberFixedPoint2.
  • 找到一组类型,D从中可以考虑使用定义的转换运算符:just{ decimal, NumberFixedPoint2 }
  • 找到适用的用户定义和提升转换运算符集U.decimal 包括 int(参见第6.4.3节),因为有从一个标准的隐式转换intdecimal.因此,明确的转换操作符U和确实的唯一成员U
  • 找到Sx运营商中最具体的源类型U
    • 操作员不会从S(decimal)转换,因此第一颗子弹出来了
    • 操作员不会从包含S(decimal包括int,而不是反过来)的类型转换,因此第二个子弹已经出来
    • 这就是第三个子弹,它谈到了"最具包容性的类型" - 好吧,我们只有一种类型,所以没关系:Sx是的int.
  • 找到Tx运营商中最具体的目标类型U
    • 操作convers直NumberFixedPoint2这样TxNumberFixedPoint2.
  • 找到最具体的转换运算符:
    • U只包含一个运算符,它确实转换SxTx,所以这是最具体的运算符
  • 最后,应用转换:
    • 如果S不是Sx,那么从一个标准的显式转换SSx执行.(所以这是decimalint.)
    • 调用最具体的用户定义转换运算符(您的运算符)
    • TTx因此没有必要在第三颗子弹的转换

以粗体显示的行是确认标准显式转换确实可行的位,只实际指定了来自不同类型的显式转换.


1至少我发现它令人惊讶.我以前没有意识到这一点.

  • @JeppeStigNielsen:Jon的分析当然是正确的.表征编译器行为的较短方法是:允许**cast*运算符在用户定义的显式或隐式转换的"任一侧"静默插入*standard*显式或隐式转换.如果您有一个用户定义的从"Sailboat"到"Mammal"的转换,那么您可以在"in"侧插入"Watercraft"转换和/或转换为插入"out"侧的"Giraffe".用户定义的转换. (8认同)
  • @JeppeStigNielsen:你可能会发现我关于这个主题的文章很有趣.http://blogs.msdn.com/b/ericlippert/archive/2007/04/16/chained-user-defined-explicit-conversions-in-c.aspx (5认同)

Ree*_*sey 24

您的第二部分(使用可空类型)似乎与当前编译器中的已知错误非常相似. 根据Connect问题的回复:

虽然我们目前没有计划在下一版Visual Studio中解决此问题,但我们计划调查Roslyn中的修复程序

因此,有希望在Visual Studio和编译器的未来版本中纠正此错误.

  • 我刚刚验证了Roslyn C#编译器中修复了这个错误. (8认同)
  • 实际上@JeppeStigNielsen 你的repro 也没有通过旧编译器的PEVerify: [IL]: Error: [offset 0x00000036][found value 'System.Decimal'][expected Int32] 堆栈上的意外类型。 (2认同)