通过编译器生成的locals以可为空的结构隐式转换为System.Double:为什么会失败?

cod*_*zen 26 c# cil nullable compiler-generated

鉴于以下内容,为什么会抛出InvalidCastException?我不明白为什么它应该在一个bug之外(这是在x86; x64与clrjit.dll中的0xC0000005崩溃).

class Program
{
    static void Main(string[] args)
    {
        MyDouble? my = new MyDouble(1.0);
        Boolean compare = my == 0.0;
    }

    struct MyDouble
    {
        Double? _value;

        public MyDouble(Double value)
        {
            _value = value;
        }

        public static implicit operator Double(MyDouble value)
        {
            if (value._value.HasValue)
            {
                return value._value.Value;
            }

            throw new InvalidCastException("MyDouble value cannot convert to System.Double: no value present.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是为以下内容生成的CIL Main():

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 3
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<valuetype Program/MyDouble> my,
        [1] bool compare,
        [2] valuetype [mscorlib]System.Nullable`1<valuetype Program/MyDouble> CS$0$0000,
        [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0001)
    L_0000: nop 
    L_0001: ldloca.s my
    L_0003: ldc.r8 1
    L_000c: newobj instance void Program/MyDouble::.ctor(float64)
    L_0011: call instance void [mscorlib]System.Nullable`1<valuetype Program/MyDouble>::.ctor(!0)
    L_0016: nop 
    L_0017: ldloc.0 
    L_0018: stloc.2 
    L_0019: ldloca.s CS$0$0000
    L_001b: call instance bool [mscorlib]System.Nullable`1<valuetype Program/MyDouble>::get_HasValue()
    L_0020: brtrue.s L_002d
    L_0022: ldloca.s CS$0$0001
    L_0024: initobj [mscorlib]System.Nullable`1<float64>
    L_002a: ldloc.3 
    L_002b: br.s L_003e
    L_002d: ldloca.s CS$0$0000
    L_002f: call instance !0 [mscorlib]System.Nullable`1<valuetype Program/MyDouble>::GetValueOrDefault()
    L_0034: call float64 Program/MyDouble::op_Implicit(valuetype Program/MyDouble)
    L_0039: newobj instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0)
    L_003e: stloc.3 
    L_003f: ldloca.s CS$0$0001
    L_0041: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault()
    L_0046: call float64 Program/MyDouble::op_Implicit(valuetype Program/MyDouble)
    L_004b: conv.r8 
    L_004c: ldc.r8 0
    L_0055: bne.un.s L_0060
    L_0057: ldloca.s CS$0$0001
    L_0059: call instance bool [mscorlib]System.Nullable`1<float64>::get_HasValue()
    L_005e: br.s L_0061
    L_0060: ldc.i4.0 
    L_0061: stloc.1 
    L_0062: ret 
}
Run Code Online (Sandbox Code Playgroud)

注意IL中的行0x2D - 0x3E.它检索MyDouble?实例,对其进行调用,GetValueOrDefault在其上调用隐式运算符,然后将结果包装在a中Double?并将其存储在编译器生成的CS$0$0001本地中.在行0x3F到0x55中,我们检索CS$0$0001值,'unwrap'通过GetValueOrDefault然后比较为0 ... 但是等待一分钟!什么是MyDouble::op_Implicit在线0x46上进行的额外调用?

如果我们调试C#程序,我们确实会看到2个调用implicit operator Double(MyDouble value),并且它是第二个调用失败,因为value未初始化.

这里发生了什么?

Eri*_*ert 43

它显然是一个C#编译器错误.谢谢你引起我的注意.

顺便提一下,让用户定义的隐式转换运算符抛出异常是一种不好的做法.文档声明隐式转换应该是那些从不抛出的转换.你确定你不希望这是一个明确的转换吗?

无论如何,回到bug.

C#3和4中的错误重现,但不是C#2中的错误.这意味着这是我的错.当我重新编写用户定义的提升隐式操作符代码以使其与表达式树lambdas一起工作时,我可能导致了这个错误.对于那个很抱歉!该代码非常棘手,显然我没有充分测试它.

代码应该做的是:

首先,重载解析试图解决==的含义.两个参数都有效的最佳==运算符是比较两个可空双精度的提升运算符.因此,应将其分析为:

Boolean compare = (double?)my == (double?)0.0; 
Run Code Online (Sandbox Code Playgroud)

(如果你编写这样的代码,那么它在C#3和4中做正确的事情.)

提升==运算符的含义是:

  • 评估两个论点
  • 如果两者都为null则结果为真 - 显然在这种情况下不会发生这种情况
  • 如果一个为null而另一个不为则结果为false
  • 如果两者都不为null,那么两者都被解包为double,并比较为double.

现在的问题是"评估左手边的正确方法是什么?"

我们这里有一个来自MyDouble的提升用户定义的转换运营商?加倍?正确的行为是:

  • 如果"my"为null,那么结果是null double?
  • 如果"my"不为null,则结果是用户定义的my.Value转换为double,然后将该double转换为double?

显然,在这个过程中出现了问题.

我将在我们的数据库中输入一个错误,但任何修复都可能会错过进入下一个Service Pack的更改的截止日期.如果我是你,我会寻找解决方法.再次,为错误道歉.

  • 这使得设计和实现编译器听起来很难. (14认同)
  • @Joan:是的,我们的QA团队都是经验丰富的C#/ VB程序员,他们整天都在努力想出打破编译器和其他工具的程序.我认为我们对提升的用户定义转换和提升的内置运算符的组合有很好的报道,但显然我们需要更多的覆盖. (5认同)

Han*_*ant 6

对我来说这肯定看起来像编译错误.IL建议编译器生成转换MyDouble的代码?转换运算符为double,然后为double?但是,当它再次使用转换运算符时,需要一个急转直下?那是坏的,错误的论证类型.也没有必要.

这篇反馈文章类似于这个错误.已经超过6年,这必定是编译器的一个棘手的部分.我确实想到了.