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

Компилятор заменяет явное преобразование на мой собственный тип с явным приведением в тип .NET?

У меня есть следующий код:

public struct Num<T>
{
    private readonly T _Value;

    public Num(T value)
    {
        _Value = value;
    }

    static public explicit operator Num<T>(T value)
    {
        return new Num<T>(value);
    }
}

...
double d = 2.5;
Num<byte> b = (Num<byte>)d;

Этот код компилируется, и это меня удивляет. Явный конвертер должен принимать только byte, а не double. Но двойник как-то принят. Когда я помещаю точку останова внутри конверта, я вижу, что value уже есть byte со значением 2. Путем кастинга от double to byte должно быть явно.

Если я декомпилирую свой EXE с помощью ILSpy, я вижу следующий код:

double d = 2.5;
Program.Num<byte> b = (byte)d;

Мой вопрос: откуда этот дополнительный прилив от byte? Почему это дополнительное место? Куда отправил мой тэг Num<byte>?

ИЗМЕНИТЬ
Структура Num<T> - это целая структура, поэтому больше нет скрытых дополнительных методов или операторов.

ИЗМЕНИТЬ
ИЛ, по просьбе:

IL_0000: nop
IL_0001: ldc.r8 2.5 // Load the double 2.5.
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1 // Once again the explicit cast to byte.
IL_000d: call valuetype GeneriCalculator.Program/Num`1<!0> valuetype GeneriCalculator.Program/Num`1<uint8>::op_Explicit(!0) 
IL_0012: stloc.1
IL_0013: ret
4b9b3361

Ответ 1

Позвольте сделать шаг назад и задать некоторые уточняющие вопросы:

Является ли эта программа законной?

public struct Num<T>
{
    private readonly T _Value;

    public Num(T value)
    {
        _Value = value;
    }

    static public explicit operator Num<T>(T value)
    {
        return new Num<T>(value);
    }
}

class Program
{
    static void Main()
    {
        double d = 2.5;
        Num<byte> b = (Num<byte>)d;
    }
}

Да.

Можете ли вы объяснить, почему актерский состав является законным?

Как отметил Кен Кин, я объясняю это здесь:

http://blogs.msdn.com/b/ericlippert/archive/2007/04/16/chained-user-defined-explicit-conversions-in-c.aspx

Вкратце: пользовательское явное преобразование может иметь встроенное явное преобразование, вставленное на "оба конца". То есть мы можем вставить явное преобразование либо из исходного выражения в тип параметра определяемого пользователем метода преобразования, либо из возвращаемого типа определяемого пользователем метода преобразования в целевой тип преобразования. (Или, в некоторых редких случаях, оба.)

В этом случае мы вставляем встроенное явное преобразование в тип параметра, байт, поэтому ваша программа такая же, как если бы вы написали:

        Num<byte> b = (Num<byte>)(byte)d;

Это желательно поведение. Двойной может быть явно преобразован в байты, поэтому двойной может также быть явно преобразован в Num<byte>.

Для получения полного объяснения прочтите раздел 6.4.5 "Пользовательские явные преобразования" в спецификации С# 4.

Почему IL генерировал вызов op_Implicit вместо op_Explicit?

Это не так; вопрос основан на ложности. Вышеуказанная программа генерирует:

  IL_0000:  nop
  IL_0001:  ldc.r8     2.5
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  conv.u1
  IL_000d:  call       valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
  IL_0012:  stloc.1
  IL_0013:  ret

Вероятно, вы смотрите на старую версию своей программы. Сделайте чистую перестройку.

Существуют ли другие ситуации, в которых компилятор С# молча вставляет явное преобразование?

Да; на самом деле это второй раз, когда этот вопрос возник сегодня. См.

Неверное преобразование типа С#?

Ответ 2

Прежде всего, позвольте взглянуть на блог г-на Липперта:

Связанные пользовательские явные преобразования в С#

Иногда компилятор 1 вставляет явное преобразование для нас:

  • Часть блога:

    ...

    Когда пользовательский явный листинг требует явного преобразования на стороне вызова или на стороне возврата, компилятор будет вставлять явные преобразования по мере необходимости.

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

    ...

Как этот вопрос, это просто одно время. Какое явное преобразование вставил компилятор, как мы пишем в следующем коде:

  • Тестировать общий метод с явным преобразованием

    public static class NumHelper {
        public static Num<T> From<T>(T value) {
            return new Num<T>(value);
        }
    }
    
    public partial class TestClass {
        public static void TestGenericMethodWithExplicitConversion() {
            double d=2.5;
            Num<byte> b=NumHelper.From((byte)d);
        }
    }
    

    а сгенерированный IL тестового метода:

    IL_0000: nop
    IL_0001: ldc.r8 2.5
    IL_000a: stloc.0
    IL_000b: ldloc.0
    IL_000c: conv.u1
    IL_000d: call valuetype Num`1<!!0> NumHelper::From<uint8>(!!0)
    IL_0012: stloc.1
    IL_0013: ret
    

Отпустите один шаг назад, чтобы проверить тест явного оператора как ваш вопрос:

  • Проверить явный оператор

    public partial class TestClass {
        public static void TestExplicitOperator() {
            double d=2.5;
            Num<byte> b=(Num<byte>)d;
        }
    }
    

    и вы уже видели IL раньше:

    IL_0000: nop
    IL_0001: ldc.r8 2.5
    IL_000a: stloc.0
    IL_000b: ldloc.0
    IL_000c: conv.u1
    IL_000d: call valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
    IL_0012: stloc.1
    IL_0013: ret
    

Вы заметили, что они очень похожи? Разница в том, что параметр !0 является общим параметром в определении типа вашего исходного кода, а !!0 в тесте общего метода - это общий параметр в определении метода. Возможно, вам стоит взглянуть на главу §II.7.1 спецификации Standard ECMA-335.

Однако, самое главное, они оба попадают в тип <uint8> (который является байтом) общего определения; и, как я уже упоминал выше, согласно blogpost г-ну Липперту, нам сообщили, что компилятор иногда вставляет явное преобразование, когда сделал, чтобы указать явно!

Наконец, как вы полагаете, это странное поведение компилятора, и позвольте мне угадать, что, возможно, вы думаете, что компилятор должен сделать:

  • Провести общий метод, указав параметр типа:

    public partial class TestClass {
        public static void TestGenericMethodBySpecifyingTypeParameter() {
            double d=2.5;
            Num<byte> b=NumHelper.From<byte>(d);
        }
    }
    

Правильно ли я понял? Во всяком случае, нас здесь интересует, опять же, ИЛ. И я не могу дождаться, когда увижу IL, это:

0PC4l.png

Ooooops.. похоже, это не то, что думал компилятор, как явный оператор.

Для конкретизации, когда мы явно указали преобразование, это довольно семантический, чтобы сказать, что мы ожидаем конвертировать одну вещь в другую, компилятор вывел это и ввел очевидное необходимое преобразование задействованных типов; и после того, как он обнаружил, что этот тип не является законным для преобразования, он жалуется, так же, как мы указали более просто неправильное преобразование, например (String)3.1415926 ...

Пожелайте теперь более полезно, не теряя при этом правильности.

1: Это мое личное выражение иногда, в блоге, на самом деле говорят по мере необходимости.


Ниже приведен некоторый тест на контрастность, когда можно ожидать преобразования типа с существующим явным оператором; и я добавил несколько комментариев в код для описания каждого случая:

double d=2.5;
Num<byte> b=(Num<byte>)d; // explicitly
byte x=(byte)d; // explicitly, as the case above

Num<byte> y=d; // no explicit, and won't compile

// d can be `IConvertible`, compiles
Num<IConvertible> c=(Num<IConvertible>)d;

// d can be `IConvertible`; 
// but the conversion operator is explicit, requires specified explicitly
Num<IConvertible> e=d;

// d cannot be `String`, won't compile even specified explicitly
Num<String> s=(Num<String>)d;

// as the case above, won't compile even specified explicitly
String t=(String)d; 

Может быть, это легче понять.

Ответ 3

Соответствующий раздел стандарта С# (ECMA-334) - это §13.4.4. Я выделил выделенные жирным шрифтом части, относящиеся к вашему коду выше.

Пользовательское явное преобразование типа S в тип T обрабатывается следующим образом:

[опущен]

  • Найдите набор применимых операторов преобразования U. Этот набор состоит из определяемого пользователем и, если S и T являются обнуляемыми, отменены неявные или явные операторы преобразования (§13.7.3), объявленные классами или структурами в D, которые преобразуются из охватываемого типа или , охватываемых S для типа, охватывающего или охватываемого T. Если U пусто, преобразование отсутствует, и возникает ошибка времени компиляции.

Термины, охватывающие и охватываемые, определены в §13.4.2.

В частности, оператор преобразования от byte до Num<byte> будет рассмотрен при литье double в Num<byte>, потому что byte (фактический тип параметра для оператора) может быть неявно преобразован в double (т.е. byte охватывает тип операнда double). Определяемые пользователем операторы, подобные этому, рассматриваются только для явных преобразований, даже если оператор помечен implicit.