Подтвердить что ты не робот

Неявное преобразование в System.Double с помощью nullable struct через сгенерированные компилятором локали: почему это не удается?

Учитывая следующее, почему возникает InvalidCastException? Я не понимаю, почему это должно быть вне ошибки (это в x86, x64 сбой с 0xC0000005 в clrjit.dll).

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.");
        }
    }
}

Вот 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 
}

Отметьте строки 0x2D - 0x3E в IL. Он извлекает экземпляр MyDouble?, вызывает на нем GetValueOrDefault, вызывает на нем неявный оператор и затем обертывает результат в Double? и сохраняет его в локальном CS$0$0001 компиляторе. В строках от 0x3F до 0x55 мы извлекаем значение CS$0$0001, "разворачиваем" через GetValueOrDefault, а затем сравниваем с 0... , НО WAIT A MINUTE! Какой дополнительный вызов MyDouble::op_Implicit выполняется в строке 0x46?

Если мы отлаживаем программу С#, мы действительно видим 2 обращения к implicit operator Double(MyDouble value), и это второй вызов, который терпит неудачу, поскольку value не инициализируется.

Что здесь происходит?

4b9b3361

Ответ 1

Это явно ошибка компилятора С#. Спасибо, что привлекли мое внимание.

Кстати, плохой практикой является пользовательский оператор неявного преобразования, который генерирует исключение; в документации указывается, что неявные преобразования должны быть теми, которые никогда не бросаются. Вы уверены, что не хотите, чтобы это было явное преобразование?

В любом случае, вернемся к ошибке.

Исправлена ​​ошибка в С# 3 и 4, но не в С# 2. Это означает, что это была моя ошибка. Я, вероятно, вызвал ошибку, когда я перечеркнул пользовательский поднятый неявный код оператора, чтобы заставить его работать с деревом выражений lambdas. Извини за это! Этот код очень сложный, и, по-видимому, я не тестировал его адекватно.

Что должен делать код:

Во-первых, разрешение перегрузки пытается разрешить value ==. Оператор best ==, для которого оба аргумента действительны, - это поднятый оператор, который сравнивает два нулевых удвоения. Поэтому его следует анализировать следующим образом:

Boolean compare = (double?)my == (double?)0.0; 

(Если вы пишете такой код, то он делает правильные вещи в С# 3 и 4.)

Значение оператора lifted ==:

  • оценить оба аргумента
  • если оба значения равны нулю, тогда результат верен - ясно, что этого не может произойти в этом случае
  • если он является нулевым, а другой - нет, тогда результат будет false
  • если оба значения не равны нулю, то оба раза распаковываются и удваиваются и сравниваются как удваиваются.

Теперь возникает вопрос: "Каков правильный способ оценить левую сторону?"

У нас есть снятый пользовательский оператор преобразования из MyDouble? удвоить?. Правильное поведение:

  • Если "my" имеет значение null, то результатом является null double?.
  • Если "my" не является нулевым, то результатом является преобразованное пользователем преобразование my.Value в double, а затем преобразование этого двойника в double?.

Ясно, что в этом процессе что-то не так.

Я буду вводить ошибку в нашей базе данных, но любое исправление, вероятно, пропустит крайний срок для внесения изменений в следующий пакет обновления. Если бы я был вами, я бы искал обходные пути. Опять же, извинения за ошибку.

Ответ 2

Это наверняка выглядит как ошибка компилятора для меня. IL предлагает компилятору генерировать код для преобразования MyDouble? к двойнику с оператором преобразования, затем к двойному?. Но принимает нос, когда он снова использует оператор преобразования на этом двойном?. Этот плохой, неправильный тип аргумента. И нет необходимости.

Эта статья обратной связи напоминает эту ошибку. Уже более 6 лет это должно быть сложной частью компилятора. Кажется, это так.