Почему в C# одно приведение может выполнять как распаковку, так и преобразование перечисления?

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

  // create boxed int
  IFormattable box = 42;       // box.GetType() == typeof(int)


  // unbox and narrow
  short x1 = (short)box;       // fails runtime :-)
  short x2 = (short)(int)box;  // OK

  // unbox and make unsigned
  uint y1 = (uint)box;         // fails runtime :-)
  uint y2 = (uint)(int)box;    // OK

  // unbox and widen
  long z1 = (long)box;         // fails runtime :-)
  long z2 = (long)(int)box;    // OK (cast to long could be made implicit)

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

(В интерфейсе IFormattable нет ничего особенного; вы также можете использовать класс object, если хотите.)

Однако сегодня я понял, что с перечислениями все по-другому (когда (и только когда) перечисления имеют один и тот же базовый тип). Вот пример:

  // create boxed DayOfWeek
  IFormattable box = DayOfWeek.Monday;    // box.GetType() == typeof(DayOfWeek)


  // unbox and convert to other
  // enum type in one cast
  DateTimeKind dtk = (DateTimeKind)box;   // succeeds runtime :-(

  Console.WriteLine(box);  // writes Monday
  Console.WriteLine(dtk);  // writes Utc

Я считаю такое поведение неудачным. На самом деле должно быть обязательно говорить (DateTimeKind)(DayOfWeek)box. Читая спецификацию C#, я не вижу оправдания этой разнице между числовыми преобразованиями и преобразованиями перечисления. Такое ощущение, что безопасность типа теряется в этой ситуации.

Считаете ли вы, что это "неопределенное поведение", которое можно было бы улучшить (без изменения спецификаций) в будущей версии .NET? Это было бы критическим изменением.

Кроме того, если поставщик любого из типов перечисления (в моем примере DayOfWeek или DateTimeKind) решит изменить базовый тип одного из типов перечисления с int на что-то другое (может быть long, short,...), то вдруг приведенный выше однократный код перестанет работать, что кажется глупым.

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

Несколько связано: Почему распаковка перечислений дает странные результаты? (распаковывает int непосредственно в перечисление)

ДОПОЛНЕНИЕ:

Хорошо, так много ответов и комментариев были сосредоточены на том, как перечисления обрабатываются «под капотом». Хотя это само по себе интересно, я хочу больше сосредоточиться на том, охвачено ли наблюдаемое поведение спецификацией C#.

Предположим, я написал тип:

struct YellowInteger
{
  public readonly int Value;

  public YellowInteger(int value)
  {
    Value = value;
  }

  // Clearly a yellow integer is completely different
  // from an integer without any particular color,
  // so it is important that this conversion is
  // explicit
  public static explicit operator int(YellowInteger yi)
  {
    return yi.Value;
  }
}

а потом сказал:

object box = new YellowInteger(1);
int x = (int)box;

тогда говорит ли спецификация С# что-нибудь о том, удастся ли это во время выполнения? Мне все равно, .NET может рассматривать YellowInteger как просто Int32 с метаданными другого типа (или как там это называется), но может ли кто-нибудь гарантировать, что .NET не "перепутает" YellowInteger и Int32 при распаковке? Итак, где в спецификации C# я могу увидеть, удастся ли (int)box (вызов моего явного метода оператора)?


person Jeppe Stig Nielsen    schedule 13.07.2012    source источник
comment
Перечисления — это, по сути, метки над своими базовыми типами. Нет неопределенного поведения. Вы просто меняете маркировку фактического значения, а не его типа.   -  person Panagiotis Kanavos    schedule 13.07.2012
comment
@PanagiotisKanavos Согласно спецификации C# существует явное приведение типов перечисления E1 к любому другому типу перечисления E2. Это не говорит о том, что приведение становится неявным, если тип перечисления E1 и тип перечисления E2 (различны, но) имеют один и тот же базовый тип. И, конечно же, поскольку всякая упаковка/распаковка исключена, вы не можете назначить DayOfWeek на DateTimeKind без явного приведения.   -  person Jeppe Stig Nielsen    schedule 13.07.2012
comment
@JeppeStigNielsen Явное приведение C# двух перечислений с одним и тем же базовым типом приводит к отсутствию приведения команд IL в результирующем IL.   -  person Adam Houldsworth    schedule 13.07.2012
comment
@JeppeStigNielsen Я исправил свой ответ ссылкой на аналогичный вопрос, принятый ответ, я считаю, охватывает основную причину.   -  person Adam Houldsworth    schedule 13.07.2012
comment
@AdamHouldsworth Да, это интересный ответ, на который вы ссылаетесь. Я все еще размышляю, совместима ли эта реализация со спецификацией C#. Я попытался вызвать Ханса Пассанта в свою ветку здесь :-) Кроме того, я обновил свой вопрос выше, приведя пример, где это тип структуры, а не тип перечисления.   -  person Jeppe Stig Nielsen    schedule 13.07.2012


Ответы (2)


Когда вы используете:

IFormattable box = 42; 
long z2 = (long)(int)box;

Вы фактически распаковываете, а затем кастинг.

Но во втором случае:

IFormattable box = DayOfWeek.Monday; 
DateTimeKind dtk = (DateTimeKind)box;

Вы вообще не проводите никаких кастингов. Вы просто распаковываете значение. Базовым типом элементов перечисления по умолчанию является int.

Обновите ссылку на настоящий вопрос:

спецификация, которую вы упомянули в комментарии. :

The explicit enumeration conversions are:
...
From any enum-type to any other enum-type.

Это на самом деле правильно. Мы не можем неявно преобразовать:

//doesn't compile
DateTimeKind dtk = DayOfWeek.Monday;

Но мы можем явно преобразовать:

DateTimeKind dtk = (DateTimeKind)DayOfWeek.Monday;

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

Обновление 2

У меня возникло ощущение, что кто-то, должно быть, заметил это раньше, зашел в Google, поискал «unboxing convert enum» и знаете что? Skeet написал об этом в блоге в 2005 году (ошибка спецификации CLI с распаковкой и перечислениями)

person doblak    schedule 13.07.2012
comment
Вы читали спецификацию §6.2.2 Явные преобразования перечисления? Обычно для преобразования между перечислимыми типами требуется явное приведение типов. Вы не можете просто присвоить DayOfWeek DateTimeKind. Тип перечисления отличается от лежащего в его основе целочисленного типа. - person Jeppe Stig Nielsen; 13.07.2012
comment
@JeppeStigNielsen Преобразование между перечислениями одного и того же базового типа фактически не приводит к приведению инструкций IL. - person Adam Houldsworth; 13.07.2012
comment
@AdamHouldsworth Хорошо, это описание того, как это реализовано, и оно согласуется с моими наблюдениями. Но я не думаю, что это согласуется с официальной спецификацией C#. Нигде в спецификации не упоминается, что явность преобразования перечисления должна зависеть от базовых целочисленных типов. - person Jeppe Stig Nielsen; 13.07.2012
comment
@JeppeStigNielsen Язык C# не является точным зеркалом результирующего IL, поэтому действие распаковки, которое не контролируется в C#, может обманывать и распаковывать в другое логическое перечисление того же базового тип. Есть вещи, которые можно сделать в IL, но не в C#. - person Adam Houldsworth; 13.07.2012
comment
@Darjan Другой способ изменить значение - public enum ShortBasedEnum : short. - person Adam Houldsworth; 13.07.2012
comment
@AdamHouldsworth Переход на уровень IL наверняка объяснит, как все реализовано. Но на самом деле мой вопрос не об этом. Компилятор C# мог бы выдать некоторый IL, который проверял бы типы в случае (DateTimeKind)box, если бы захотел. Информация где-то есть. Но компилятор (и/или среда выполнения) в этом случае забывает проверить безопасность типов. Мой вопрос не: как это реализовано? Вместо этого у меня возникает вопрос : не противоречит ли эта реализация спецификации языка C#? - person Jeppe Stig Nielsen; 13.07.2012
comment
@JeppeStigNielsen Извините, я взял заголовок вашего вопроса как вопрос, а не тот, что в середине вашего текста. - person Adam Houldsworth; 13.07.2012
comment
@AdamHouldsworth Я могу понять ваше замешательство. Возможно, в C# одно приведение может выполнять как распаковку, так и преобразование перечисления, но в спецификации не упоминается, что это было бы лучшим названием (хотя и слишком длинным). - person Jeppe Stig Nielsen; 13.07.2012
comment
@JeppeStigNielsen: Документация по преобразованиям перечисления кажется очень хорошей. Распаковка здесь опущена, в этом вы правы. - person doblak; 13.07.2012
comment
@AdamHouldsworth, Jeppe: посмотрите последнее обновление и пост Скита, он все объяснил. - person doblak; 13.07.2012
comment
Вероятно, больше ответов не будет, поэтому я приму этот, который стал нормальным после того, как вы его отредактировали. Я все еще думаю, что это позор, что это происходит. Даже если два перечисления занимают одинаковое количество бит, они все равно относятся к разным типам. И этого не происходит при распаковке int в uint или YellowInteger в int, хотя все эти типы значений представляют собой всего лишь 32 бита данных с разными метаданными. - person Jeppe Stig Nielsen; 20.07.2012

Это связано с тем, что они фактически представлены как базовый тип значения во время выполнения. Они оба int, что соответствует той же ситуации, что и ваши неудачные случаи - если вы измените тип перечисления в этой ситуации, это также не удастся.

Поскольку тип тот же, действие просто распаковывает файл int.

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

Обновить

Если вы создадите некоторый код, который приводит int перечислений друг к другу, вы увидите, что в сгенерированном IL нет действий приведения. Когда вы упаковываете перечисление и распаковываете его в другой тип, есть только действие unbox.any, которое:

Преобразует упакованное представление типа, указанного в инструкции, в его неупакованную форму.

В данном случае это каждое из перечислений, но оба они int.

Обновление 2:

Я достиг предела возможности объяснить, что здесь происходит, без более глубоких исследований с моей стороны, но я заметил этот вопрос:

Как получилось, что перечисление происходит от System.Enum и в то же время является целым числом?

Это может немного объяснить, как обрабатываются перечисления.

person Adam Houldsworth    schedule 13.07.2012
comment
Но тип перечисления не теряется, когда я упаковываю перечисление. Если у меня есть IFormattable box1 = DayOfWeek.Monday;, IFormattable box2 = DateTimeKind.Utc; и IFormattable box3 = (int)1;, то все эти ящики имеют разные GetType(), и ни один из них не будет сравниваться равным под Object.Equals. - person Jeppe Stig Nielsen; 13.07.2012
comment
@JeppeStigNielsen Независимо от метаданных, базовым типом значения перечисления является int. Очевидно, существует некоторая дополнительная поддержка перечислений в отражении, так же как и поддержка Nullable<T>, которая является struct, для содержания нулевых значений. - person Adam Houldsworth; 13.07.2012
comment
Но где в спецификации сказано: (1) преобразование распаковки и числовое преобразование нельзя объединять в одно приведение. (2) Преобразование распаковки и преобразование перечисления можно объединить в одно приведение? - person Jeppe Stig Nielsen; 13.07.2012
comment
@JeppeStigNielsen Поскольку вы не выполняете кастинг, вы заменяете что-то, что является int, на что-то, что является int. Как я уже сказал, если вы измените одно из этих перечислений на not-int и попробуете то же самое, оно потерпит неудачу по тем же причинам. - person Adam Houldsworth; 13.07.2012