Компилятор заменяет явное приведение к моему собственному типу явным приведением к типу .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 к byte должно быть явным.

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

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

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

EDIT
Структура Num<T> – это вся структура, поэтому больше никаких скрытых дополнительных методов или операторов.

ИЗМЕНИТЬ
IL в соответствии с запросом:

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

person Martin Mulder    schedule 07.05.2013    source источник
comment
Возможно, вы захотите взглянуть на необработанный IL, а не на C#.   -  person Kirk Woll    schedule 07.05.2013
comment
@Kirk: IL добавлен. Делает это еще более запутанным! :)   -  person Martin Mulder    schedule 07.05.2013
comment
Мне кажется, что строка IL_000d делает именно то, что вы хотите, и что декомпиляция C# была ошибочной.   -  person Kirk Woll    schedule 07.05.2013
comment
Я не согласен. откуда conv.u1? И с каких это пор мой оператор переведен на op_Implicit а не на op_Explicit?   -  person Martin Mulder    schedule 07.05.2013
comment
Двойной наследует байт??? Если да, то я не удивлюсь.   -  person Daniel Möller    schedule 07.05.2013
comment
@ Даниэль Конечно нет. Они оба являются типами значений, поэтому они оба могут наследоваться только от ValueType. double содержит больше информации, чем байт, поэтому неявного преобразования в byte нет, это операция с потерями. Существует явное преобразование, но оно явно не вызывается.   -  person Servy    schedule 07.05.2013
comment
Я вижу, как это произошло. Мы ничего не можем с этим поделать. Вам нужно будет зарегистрировать это на connect.microsoft.com. Количество инженерных работ, необходимых для его исправления, вероятно, будет значительным, поэтому создание случая, когда вы не можете найти обходной путь, будет затруднено.   -  person Hans Passant    schedule 07.05.2013
comment
@HansPassant: здесь нечего исправлять. Компилятор ведет себя корректно. (Есть тонкие ошибки, которые команда компилятора C# не исправит в пользовательских явных преобразованиях, но это не одна из них.)   -  person Eric Lippert    schedule 07.05.2013
comment
Хм, подключение по-прежнему является довольно хорошим способом проникнуть в Редмондский пузырь. Конечно, продукт, над которым вы работаете, будет помечать это заявление? Или вы собираетесь пропустить это, потому что это законно?   -  person Hans Passant    schedule 08.05.2013


Ответы (3)


Давайте сделаем шаг назад и зададим несколько уточняющих вопросов:

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

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;
    }
}

да.

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

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

Связанный пользователем явные преобразования в C#

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

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

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

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

Полное объяснение см. в разделе 6.4.5 Пользовательские явные преобразования в спецификации C# 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

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

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

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

преобразование типа C# несовместимо?

person Eric Lippert    schedule 07.05.2013
comment
@Eric: О моей путанице op_Explicit и op_Implict: вы правы, я исправил свой вопрос. - person Martin Mulder; 08.05.2013
comment
@Eric: Правильно ли я понимаю, когда говорю: когда компилятор должен выполнить явное преобразование (потому что так сказано в коде), он может свободно использовать другое явное преобразование, чтобы выполнить работу? - person Martin Mulder; 08.05.2013
comment
@MartinMulder: Правильно. Как я показал в своем блоге, вы действительно можете получить четыре явных конверсии по цене одной в некоторых надуманных случаях. Однако вы никогда не получите две определенные пользователем конверсии по цене одной. Если у Animal есть UDC для Fruit, а у Fruit есть UDC для Shape, вы можете разыграть Giraffe в Apple и Apple в Triangle, но вы не можете разыграть Giraffe напрямую в Triangle. - person Eric Lippert; 08.05.2013
comment
@Эрик: Спасибо. Но... на этом история еще не закончилась... Сегодня задам второй вопрос на эту же тему. Я буду держать вас в курсе. (Кстати: ваш первый пример кода в вашем блоге не совсем корректен. В операторе отсутствует параметр типа Castle.) - person Martin Mulder; 08.05.2013
comment
@MartinMulder: Поскольку вы, возможно, видели некоторые книги или даже загружали образцы кода, они оставляли некоторые тонкие отсутствующие или ошибки, все в порядке. Эти тонкие детали особенно привлекли ваше внимание. - person Ken Kin; 08.05.2013
comment
@Eric Lippert: В своей статье вы написали, что пользовательское преобразование может иметь встроенные преобразования, автоматически вставленные на стороне вызова и возврата, но мы никогда не вставляем автоматически другие пользовательские преобразования. Есть ли причина, по которой системные явные преобразования обрабатывается иначе, чем явные преобразования пользователя? - person Martin Mulder; 10.05.2013
comment
@MartinMulder: Потому что тогда для этих пользовательских преобразований могут потребоваться преобразования с обеих сторон! А теперь нам нужно решить еще одну проблему с разрешением перегрузки, которая может привести к еще большему количеству проблем с разрешением перегрузки. И каковы шансы, что результат, который выйдет на другом конце, будет той цепочкой конверсий, которую планировал пользователь? На самом деле довольно низко. Функция, которую вы описываете, будет фермой ошибок как для разработчиков компилятора, так и для разработчиков, использующих эту функцию. - person Eric Lippert; 10.05.2013

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

связанный определяемые пользователем явные преобразования в C#

Компилятор будет иногда1 вставлять за нас явное преобразование:

  • Part of the blogpost:

    ...

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

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

    ...

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

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

    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 спецификации Стандарта ECMA-335. .

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

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

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

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

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

0PC4l.png

Оооооо... кажется, это не так, как думал компилятор, как будет вести себя явный оператор.

В заключение, когда мы указали преобразование явно, довольно семантически сказать, что мы ожидаем преобразовать одно в другое, компилятор вывел это и вставил очевидное необходимое преобразование задействованных типов; и как только он обнаружил, что используемый тип не может быть преобразован, он жалуется, точно так же, как мы указали более просто неправильное преобразование, такое как (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; 

Может так проще понять.

person Ken Kin    schedule 07.05.2013
comment
Я не понимаю вашего ответа. Пожалуйста, дополните. - person Kirk Woll; 07.05.2013
comment
Как могло произойти Num<byte> b=(Num<byte>)d? Приведение от double к Num<byte> не существует. И я не понимаю, что вы пытаетесь сказать третьей строкой кода. - person Martin Mulder; 07.05.2013
comment
@Ken: Я вижу, вы отредактировали свой ответ, но все же ... явного преобразования из double в Num<byte> НЕТ. Явное приведение типа double к byte ДЕЙСТВИТЕЛЬНО существует. Таким образом, строки 2 не должны компилироваться, и да, строка 3 будет скомпилирована. - person Martin Mulder; 07.05.2013
comment
К вашему редактированию, существует неявное преобразование из double в IConvertible, так что это имеет смысл, но есть только явное преобразование из double в byte, что очень другое дело. - person Servy; 07.05.2013
comment
Я не понимаю, почему этот ответ был отклонен. Это правильно. - person Eric Lippert; 07.05.2013
comment
@EricLippert Да, это правильно, но это не полезно. Он просто сбрасывает некоторый код и делает несколько утверждений. Да, эти утверждения верны, но они не отвечают на вопрос и не объясняют поведение, которого не ожидает ОП, или почему оно ведет себя именно так. Из-за этого ответ правильный, но не полезный, поэтому голосование против. О, и также обратите внимание, что ответ был гораздо менее полезным в предыдущих версиях, когда он привлек отрицательные голоса. Теперь это несколько полезнее. - person Servy; 07.05.2013
comment
@Servy: я когда-то думал, что мое объяснение в качестве комментария к коду более понятно, чем перемещение этих слов дальше от кода .. - person Ken Kin; 07.05.2013
comment
@KenKin Я знаю, что у вас есть комментарии к коду. По сути, это просто список того, будет ли компилироваться строка. Опять же, вы объясняете, что делает код (и даже здесь совсем не подробно), в то время как вопрос спрашивает почему. Вы не отвечаете на вопрос почему. - person Servy; 08.05.2013
comment
@KenKin Из самого вопроса очевидно, что компилятор добавляет явное преобразование из двойного в байтовое, несмотря на то, что оно не указано в коде. Почему компилятор неявно добавляет это явное преобразование? Вот в чем вопрос. Самое большее, что говорит ваш ответ, это то, что он добавляет его. Ну, дух, это то, что ОП понял в вопросе. Почему компилятор это делает? это вопрос. Что вы на это ответите? - person Servy; 08.05.2013
comment
Единственная информация в этом ответе, которую я считаю полезной, — это ссылка, которая на самом деле объясняет точно то, что хочет знать ОП. Остальное все шум. Это информация, которая либо не имеет отношения к делу (даже если она верна), либо уже есть в OP. Если бы вы просто эффективно резюмировали связанную статью, у вас был бы прекрасный ответ. Имейте в виду, что ценный контент в ответе должен быть в ответе, а не в ссылке. - person Servy; 08.05.2013
comment
@Servy: Ах ... может быть, мне следует процитировать какое-то объяснение из блога мистера Липперта. Спасибо большое. - person Ken Kin; 08.05.2013
comment
@KenKin Я также думаю, что ваш ответ можно улучшить, удалив большую часть, если не все, что у вас есть в блоках кода. Это просто воспроизведение того, что видит ОП, которое не отвечает на его вопросы, и добавляет шум, который отвлекает от того, что вы, надеюсь, добавите, чтобы действительно ответить на вопрос. - person Servy; 08.05.2013
comment
@Servy Я отрицаю ответы только тогда, когда они совершенно неверны или непонятны. Я голосую за ответы, когда они правильные и полезные. Я не голосую за ответы, которые являются правильными, но не очень полезными, или полезными, но немного ошибочными. Текущий ответ Кена Кина правильный и полезный. - person Daniel A.A. Pelsmaeker; 08.05.2013
comment
@Ken Kin: Как отмечают другие люди. Ваш первый ответ (всего 3 строки кода и 1 строка на английском языке) был далек от того, чтобы быть полезным ответом. Это все еще не очень полезно. SO заявляет, что полезный ответ должен стоять сам по себе. Теперь у вас есть ссылка на другую страницу с объяснением и в вашем ответе только несколько примеров. Но в вашем тексте толком ничего не объяснено. Если другие страницы будут удалены, ваш ответ развалится. - person Martin Mulder; 08.05.2013
comment
@Virtlink Вы, безусловно, можете голосовать так, как хотите; Я не имею права сказать, что вы не можете голосовать, используя эту метрику; Я просто объяснял вероятные рассуждения для других, которые проголосовали за пост Кена Кина, чтобы он знал, как он может его улучшить. Также обратите внимание, что во всплывающей подсказке для кнопок голосования конкретно указано, что этот пост полезен для голосования, а этот пост бесполезен для минусов, так что это предполагаемая метрика системы. Хотя правильность связана с полезностью, не все правильные сообщения полезны, они просто должны быть правильными, чтобы быть полезными. - person Servy; 08.05.2013

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

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

[опущено]

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

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

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

person Sam Harwell    schedule 07.05.2013
comment
Мне никогда не нравилась эта часть спецификации; все понятие охвата испорчено. Например: double охватывает byte, и поэтому в этом примере мы видим, что вы можете вставить встроенное преобразование из double в byte перед пользовательским преобразованием. Но double не охватывается десятичным числом и не включает его, потому что ни одно из них неявно не может быть преобразовано в другое, даже если оба явно преобразуются в другое. Почему охватывающее отношение уместно здесь? Конечно, релевантное отношение — это отношение, которое можно явно преобразовать в отношение. Все дело в беспорядке. - person Eric Lippert; 07.05.2013
comment
Что означает D? Казалось бы, это не может быть набор с открытым концом, поскольку в противном случае количество возможных операторов преобразования было бы безграничным, но что это такое? - person supercat; 08.05.2013
comment
@EricLippert Это указывало бы на то, что преобразование (Num<decimal>)d (где d является double) недействительно. (Моя точка зрения заключается в том, что хотя это может показаться странным или неинтуитивным, семантика, похоже, четко определена этой частью спецификации.) - person Sam Harwell; 08.05.2013
comment
@supercat: D определяется в разделе спецификации, который здесь не цитируется. Если у нас есть преобразование из S в T, то D — это S0, T0 и все базовые классы S0 и T0. S0 и T0 являются либо S и T, либо их базовыми типами, если они допускают значение NULL. Так что это множество конечно. Однако ваша критика применима в другом месте; бывают ситуации, когда спецификация подразумевает, что разрешение перегрузки выбирается из бесконечного набора, что явно не так. Мэдс знает о них. - person Eric Lippert; 08.05.2013
comment
@ 280Z28: правильно; что преобразование не является законным. Но почему явный double-to-Num<byte> должен быть допустимым, потому что byte to double является неявным? Конечно, релевантной точкой решения будет то, что это законно, потому что удвоение байта является явным. Эта часть спецификации довольно четко определена, это просто причудливый выбор предиката. - person Eric Lippert; 08.05.2013
comment
@EricLippert: имеет смысл, что компилятор не будет искать в типах, производных из T, даже операторы преобразования, определенные в этих типах, могут лучше соответствовать S, поскольку компилятор не знает, как идентифицировать все типы кандидатов. Однако кажется странным, что этот набор будет включать в себя базовые типы T, поскольку это, казалось бы, открывает возможность неоднозначных преобразований. Если существуют преобразования из Double в базовый тип T и из Byte в T, что будет предпочтительнее, если кто-то попытается преобразовать Double в T, или компилятор откажется сделать то и другое? - person supercat; 08.05.2013
comment
@supercat: Чтобы ответить на ваш первый вопрос: как и в любой проблеме разрешения перегрузки, все кандидаты помещаются в набор, и разрешение перегрузки пытается найти уникальный лучший элемент этого набора. Он немного отличается от обычного разрешения перегрузки тем, что обычное разрешение перегрузки не учитывает тип возвращаемого значения, но идея та же. Точные данные указаны в спецификации. - person Eric Lippert; 08.05.2013
comment
@EricLippert: не по теме: я действительно спросил stackoverflow.com/questions/14619574/ 3 месяца назад, не получив подходящего ответа. Я только что прочитал всю серию ericlippert.com/2013. /05/06/production-permutations-part-seven, вы уже ответили на этот вопрос в блоге и даже больше. Большое спасибо! - person Ken Kin; 08.05.2013
comment
@KenKin: Добро пожаловать! Я давно хотел написать эту серию. - person Eric Lippert; 08.05.2013