59

我认为这个问题会让我在 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 */
}

它的编写方式是只允许不丢失精度的安全转换。但是,当我尝试此代码时:

    static void Main()
    {
        decimal bad = 2.718281828m;
        NumberFixedPoint2 badNfp2 = (NumberFixedPoint2)bad;
        Console.WriteLine(badNfp2);
    }

我很惊讶它会编译,并且在运行时会写出2. 从int(of value 2) 到的转换NumberFixedPoint2在这里很重要。(如果有人想知道,最好WriteLine使用 a的重载。)System.Decimal

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

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

它变得更糟。试试这个代码:

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

如您所见,我们在Nullable<>这里有(提升)转化率。但是哦,是的,那确实编译。

x86 “平台”中编译时,此代码会写出一个不可预测的数值。哪一个因时而异。例如,有一次我得到了2289956. 现在,这是一个严重的错误!

当为x64平台编译时,上面的代码使应用程序崩溃,并显示一条System.InvalidProgramException消息Common Language Runtime detected an invalid program。根据InvalidProgramException该类的文档:

通常,这表明生成程序的编译器中存在错误。

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

4

2 回答 2

44

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

只有从to的显式转换,但该转换在您的代码中被隐式调用。转换发生在这个 IL 中:decimalint

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)

我相信这是根据规范的正确行为,即使它令人惊讶1。让我们逐步了解 C# 4 规范(用户定义的显式转换)的第 6.4.5 节。我不打算复制所有文本,因为这会很乏味 - 只是我们案例中的相关结果。同样,我不打算使用下标,因为它们在这里不能很好地使用代码字体:)

  • 确定类型S0and T0: S0is decimal, and T0is NumberFixedPoint2
  • 找到类型集D,从中将考虑使用定义的转换运算符:只是{ decimal, NumberFixedPoint2 }
  • 找到一组适用的用户定义和提升的转换运算符,Udecimal 包含 int(第 6.4.3 节),因为存在从intto的标准隐式转换decimal。所以显式转换运算符in U,并且确实是 的唯一成员U
  • 找到最具体的源类型 ,Sx中的运算符U
    • 运算符不会从S( decimal) 转换,因此第一个项目符号出现了
    • 运算符不会从包含Sdecimal包含int,而不是相反)的类型转换,因此第二个项目符号出现了
    • 剩下的第三个项目符号是关于“最包容的类型”——好吧,我们只有一种类型,所以没关系:Sxis int
  • 找到最具体的目标类型 ,Tx中的运算符U
    • NumberFixedPoint2操作员Tx直接交谈NumberFixedPoint2
  • 查找最具体的转换运算符:
    • U只包含一个运算符,它确实从Sxto转换Tx,所以这是最具体的运算符
  • 最后,应用转换:
    • 如果S不是,则执行从toSx的标准显式转换。SSx(那就是decimalint
    • 调用最具体的用户定义转换运算符(您的运算符)
    • T所以Tx不需要在第三个项目符号中进行转换

粗体线是确认标准显式转换确实可行的位,当实际仅指定来自不同类型的显式转换时。


1好吧,至少我觉得这很令人惊讶。我以前不知道看到这个。

于 2013-08-20T19:15:11.017 回答
24

您的第二部分(使用可空类型)似乎与当前编译器中的这个已知错误非常相似。 从对 Connect 问题的响应中:

虽然我们目前没有计划在下一版本的 Visual Studio 中解决此问题,但我们确实计划研究 Roslyn 中的修复

因此,这个错误有望在 Visual Studio 和编译器的未来版本中得到纠正。

于 2013-08-20T19:07:41.907 回答