Как получается, что перечисление происходит от System.Enum и является одновременно целым числом? - PullRequest
41 голосов
/ 07 января 2011

Редактировать : комментарии внизу. Также это .


Вот что меня смущает. Насколько я понимаю, если у меня есть такое перечисление ...

enum Animal
{
    Dog,
    Cat
}

... то, что я по существу сделал, определено типом значения , называемым Animal с двумя определенными значениями, Dog и Cat. Этот тип является производным от ссылочного типа System.Enum (то, что типы значений обычно не могут делать - по крайней мере, в C # - но это разрешено в этом случае), и имеет средство для возврата и вперед / от int значений.

Если бы способ, которым я только что описал тип перечисления выше, был верным, то я ожидал бы, что следующий код выдаст InvalidCastException:

public class Program
{
    public static void Main(string[] args)
    {
        // Box it.
        object animal = Animal.Dog;

        // Unbox it. How are these both successful?
        int i = (int)animal;
        Enum e = (Enum)animal;

        // Prints "0".
        Console.WriteLine(i);

        // Prints "Dog".
        Console.WriteLine(e);
    }
}

Как правило, вы не можете распаковать тип значения из System.Object как что-либо, кроме его точного типа . Итак, как это возможно? Это как если бы Animal тип был int (не только конвертируемый в int), а - Enum (не только конвертируемый в Enum) одновременно. Это множественное наследование? System.Enum как-то наследуется от System.Int32 (что-то, чего я не ожидал, возможно)?

Редактировать : Это не может быть ни одним из вышеперечисленных. Следующий код демонстрирует это (я думаю) убедительно:

object animal = Animal.Dog;

Console.WriteLine(animal is Enum);
Console.WriteLine(animal is int);

Вышеуказанные выходы:

True
False

Как в документации MSDN по перечислениям , так и в спецификации C # используется термин "базовый тип"; но я не знаю, что это значит, и никогда не слышал, чтобы это использовалось в отношении чего-либо, кроме перечислений. Что на самом деле означает «базовый тип» ?


Итак, это еще один случай, который получает специальную обработку от CLR ?

Мои деньги в этом случае ... но ответ / объяснение было бы неплохо.


Обновление : Damien_The_Unbeliever предоставил ссылку, чтобы действительно ответить на этот вопрос. Объяснение можно найти в Разделе II спецификации CLI, в разделе о перечислениях:

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

Редактировать (снова?!) : Подождите, на самом деле, я не знаю, правильно ли я прочитал это в первый раз. Возможно, это не на 100% объясняет само специализированное поведение при распаковке (хотя я оставляю ответ Дэмиена как принятый, поскольку он пролил много света на эту проблему). Я продолжу изучать это ...


Еще один правка : Чувак, тогда ответ yodaj007 бросил меня на еще одну петлю. Каким-то образом перечисление не совсем то же самое, что и int; все же int можно присвоить переменной enum без приведения ? Буг?

Я думаю, что все это в конечном итоге освещается ответом Ганса , поэтому я принял его. (Извини, Дэмиен!)

Ответы [ 8 ]

23 голосов
/ 07 января 2011

Да, особый режим.JIT-компилятор хорошо знает, как работают упакованные типы значений.Что в целом делает типы значений немного шизоидными.Бокс включает в себя создание значения System.Object, которое ведет себя точно так же, как значение ссылочного типа.В этот момент значения типа значения больше не ведут себя так же, как значения во время выполнения.Что позволяет, например, иметь виртуальный метод, такой как ToString ().В штучной упаковке объект имеет указатель таблицы методов, как это делают ссылочные типы.

Компилятору JIT известны указатели таблиц методов для типов значений, таких как int и bool.Упаковка и распаковка для них очень эффективны, для этого требуется всего лишь несколько инструкций машинного кода.Это должно было быть эффективным в .NET 1.0, чтобы сделать его конкурентоспособным. очень важной частью этого является ограничение, что значение типа значения может быть распаковано только для того же типа.Это позволяет избежать дрожания от необходимости генерировать массивный оператор switch, который вызывает правильный код преобразования.Все, что нужно сделать, это проверить указатель таблицы методов в объекте и убедиться, что это ожидаемый тип.И скопируйте значение из объекта напрямую.Возможно, примечательно, что это ограничение не существует в VB.NET, его оператор CType () фактически генерирует код для вспомогательной функции, которая содержит этот большой оператор switch.

Проблема с типами Enum заключается в том, чтоне может работатьПеречисления могут иметь другой тип GetUnderlyingType ().Другими словами, распакованное значение имеет разные размеры, поэтому простое копирование значения из упакованного объекта не может работать.Хорошо понимая, что джиттер больше не встроен в код распаковки, он генерирует вызов вспомогательной функции в CLR.

Этот помощник называется JIT_Unbox (), его исходный код можно найти в источнике SSCLI20, clr / src / vm / jithelpers.cpp.Вы увидите, что он имеет дело с типами enum специально.Это разрешительно, это позволяет распаковывать от одного типа перечисления к другому.Но только если базовый тип такой же, вы получите InvalidCastException, если это не так.

По этой же причине Enum объявлен как класс.Его логическое поведение относится к ссылочному типу, производные типы перечислений могут быть преобразованы из одного в другой.С указанным выше ограничением на совместимость базовых типов.Значения типа enum, однако, очень похожи на значения типа value.У них есть семантика копирования и поведение в боксе.

9 голосов
/ 07 января 2011

Перечисления специально обрабатываются CLR.Если вы хотите вникнуть в подробности, вы можете скачать спецификацию MS Partition II .В нем вы найдете, что Enums:

Enums подчиняются дополнительным ограничениям, помимо ограничений других типов значений.Перечисления должны содержать только поля в качестве членов (они даже не должны определять инициализаторы типов или конструкторы экземпляров);они не должны реализовывать какие-либо интерфейсы;они должны иметь автоматическое расположение полей (§10.1.2);они должны иметь ровно одно поле экземпляра, и оно должно иметь базовый тип перечисления;все остальные поля должны быть статическими и литеральными (§16.1);

Так вот, как они могут наследовать от System.Enum, но иметь «базовый» тип - это разрешено только одно поле экземпляраиметь.

Существует также обсуждение поведения бокса, но оно не описывает явное распаковывание базового типа, которое я вижу.

4 голосов
/ 07 января 2011

Раздел I, 8.5.2 гласит, что перечисления являются «альтернативным именем для существующего типа», но «[f] или в целях сопоставления подписей перечисление не должно совпадать с базовым типом».

Раздел II, 14.3, разъясняет: «Для всех других целей, включая проверку и выполнение кода, распакованное перечисление свободно взаимопревращается с его базовым типом. Перечисления могут быть упакованы в соответствующий тип экземпляра в штучной упаковке, но этот тип не тот же как упакованный тип базового типа, так бокс не теряет первоначальный тип перечисления. "

Раздел III, 4.32 объясняет поведение распаковки: «Тип тип значения , содержащийся в obj , должен быть совместимым с присвоением с valuetype . [Примечание: это влияет на поведение типов enum, см. раздел II.14.3. примечание к концу] "

4 голосов
/ 07 января 2011

Я отмечаю здесь со страницы 38 ECMA-335 (я предлагаю вам скачать его только для того, чтобы иметь его):

CTS поддерживает перечисление (также называемое типом перечисления), альтернативное имя для существующего типа. В целях сопоставления подписей перечисление не должно совпадать с базовым типом. Однако экземпляры перечисления должны присваиваться базовому типу и наоборот. То есть не требуется преобразование (см. §8.3.3) или принуждение (см. §8.3.2) для преобразования из перечисления в базовый тип, равно как и из базового типа в перечисление. Перечисление значительно более ограничено, чем истинный тип, как показано ниже:

Базовый тип должен быть встроенным целочисленным типом. Перечисления должны быть производными от System.Enum, поэтому они являются типами значений. Как и все типы значений, они должны быть запечатаны (см. §8.9.9).

enum Foo { Bar = 1 }
Foo x = Foo.Bar;

Это утверждение будет ложным из-за второго предложения:

x is int

Они являются одинаковыми (псевдоним), но их подпись не совпадает. Преобразование в int и обратно не является приведением.

со страницы 46:

базовые типы - в перечислениях CTS есть альтернативные имена для существующих типов (§8.5.2), называемые их базовым типом. За исключением соответствия подписи (§8.5.2) перечисления обрабатываются как их основной тип. Это подмножество является набором типов хранения с удаленными перечислениями.

Вернитесь к моему перечислению Foo ранее. Это утверждение будет работать:

Foo x = (Foo)5;

Если вы проверяете сгенерированный код IL моего метода Main в Reflector:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
    [0] valuetype ConsoleTesting.Foo x)
L_0000: nop 
L_0001: ldc.i4.5 
L_0002: stloc.0 
L_0003: call string [mscorlib]System.Console::ReadLine()
L_0008: pop 
L_0009: ret 
}

Обратите внимание, что актеров нет. ldc находится на стр. 86. Загружает постоянную. i4 находится на стр. 151, указывая, что тип является 32-разрядным целым числом. Там нет актерского состава!

3 голосов
/ 07 января 2011

Извлечено из MSDN :

Базовым типом по умолчанию для элементов перечисления является int.По умолчанию первый перечислитель имеет значение 0, а значение каждого последующего перечислителя увеличивается на 1.

Итак, приведение возможно, но вы должны принудительно вызвать его:

Базовый тип указывает, сколько памяти выделяется для каждого перечислителя.Однако для преобразования из типа enum в целочисленный тип требуется явное приведение.

Когда вы помещаете ваше перечисление в object, объект животного получается из System.Enum (реальный типизвестно во время выполнения), так что на самом деле это int, поэтому приведение действительно.

  • (animal is Enum) возвращает true: по этой причине вы можете распаковать животное в Enum или событие в intвыполнение явного приведения.
  • (animal is int) возвращает false: Оператор is (в общем случае проверка типа) не проверяет базовый тип для Enums.Кроме того, по этой причине вам необходимо выполнить явное приведение для преобразования Enum в int.
2 голосов
/ 07 января 2011

В то время как типы enum наследуются от System.Enum, любое преобразование между ними не является прямым, а может быть боксом / распаковкой.Из C # 3.0 Спецификация:

Тип перечисления - это отдельный тип с именованными константами.Каждый тип перечисления имеет базовый тип, который должен быть байтом, sbyte, коротким, ushort, int, uint, long или ulong.Набор значений типа перечисления совпадает с набором значений базового типа.Значения типа перечисления не ограничиваются значениями именованных констант.Типы перечисления определяются через объявления перечисления

Итак, хотя ваш класс Animal является производным от System.Enum, он на самом деле int.Кстати, еще одна странная вещь - System.Enum является производным от System.ValueType, однако это все еще ссылочный тип.

1 голос
/ 28 февраля 2014

Почему бы и нет ... это вполне допустимо, например, для структуры, которая содержит внутреннее int и может быть преобразована в int с явным оператором приведения ... давайте смоделируем Enum:

interface IEnum { }

struct MyEnumS : IEnum
{
    private int inner;

    public static explicit operator int(MyEnumS val)
    {
        return val.inner;
    }

    public static explicit operator MyEnumS(int val)
    {
        MyEnumS result;
        result.inner = val;
        return result;
    }

    public static readonly MyEnumS EnumItem1 = (MyEnumS)0;
    public static readonly MyEnumS EnumItem2 = (MyEnumS)2;
    public static readonly MyEnumS EnumItem3 = (MyEnumS)10;

    public override string ToString()
    {
        return inner == 0 ? "EnumItem1" :
            inner == 2 ? "EnumItem2" :
            inner == 10 ? "EnumItem3" :
            inner.ToString();
    }
}

Эта структура может использоваться точно так же, как структура может ... конечно, если вы попытаетесь отразить тип и вызвать свойство IsEnum, он вернет false.

Давайте посмотрим на некоторое сравнение использования с эквивалентным перечислением:

enum MyEnum
{
    EnumItem1 = 0,
    EnumItem2 = 2,
    EnumItem3 = 10,
}

Сравнение использования:

Версия Struct:

var val = MyEnum.EnumItem1;
val = (MyEnum)50;
val = 0;
object obj = val;
bool isE = obj is MyEnum;
Enum en = val;

Версия перечисления:

var valS = MyEnumS.EnumItem1;
valS = (MyEnumS)50;
//valS = 0; // cannot simulate this
object objS = valS;
bool isS = objS is MyEnumS;
IEnum enS = valS;

Некоторые операции не могут быть смоделированы, но все это показывает, что я намеревался сказать ... Перечисления особенные, да ... насколько особенные? не так много! =)

1 голос
/ 07 января 2011
Базовый тип

A Enum - это тип, используемый для хранения значения констант.В вашем примере, даже если вы явно не определили значения, C # делает это:

enum Animal : int
{
    Dog = 0,
    Cat = 1
}

Внутренне, Animal состоит из двух констант с целыми значениями 0 и 1. Поэтомуможет явно привести целое число к Animal и Animal к целому числу.Если вы передаете Animal.Dog параметру, который принимает Animal, то, что вы действительно делаете, это передаете 32-битное целочисленное значение Animal.Dog (в данном случае 0).Если вы дадите Animal новый базовый тип, то значения будут сохранены как этот тип.

...