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

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

Изменить: комментарии внизу. Кроме того, this.


Вот что меня смущает. Я понимаю, что если у меня есть перечисление вроде этого...

enum Animal
{
    Dog,
    Cat
}

... то, что я по существу сделал, определяется тип значения, называемый Animal с двумя определенными значениями, Dog и Cat. Этот тип происходит от ссылочного типа System.Enum (то, что обычно не подходит для типов значений, по крайней мере, не в С#, но это разрешено в этом случае), и имеет возможность для отбрасывания назад и вперед в/из 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 a int (не просто конвертируемый в int), а - a Enum (не просто конвертируемый в Enum) в то же время. Является ли это множественным наследованием? Может ли System.Enum наследовать от System.Int32 (что-то, чего я не ожидал бы быть возможным)?

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

object animal = Animal.Dog;

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

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

True
False

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


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

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


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

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

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


Другое Edit: Man, затем yodaj007 ответ бросил меня в другой цикл. Каким-то образом перечисление не совсем то же самое, что и int; но int можно присвоить переменной перечисления без заливки? Буг?

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Основной тип должен быть встроенным целочисленным типом. Перечисления должны выводиться из 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-разрядное целое число. Нет ролика!

Ответ 4

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

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

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

Ответ 5

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

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

Итак, бросок возможен, но вам нужно его заставить:

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

Почему бы и нет... это совершенно справедливо, например, для структуры, которая должна содержать 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,
}

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

Версия структуры:

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

Версия Enum:

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

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