How is it that an enum derives from System.Enum and is an integer at the same time?

烈酒焚心 提交于 2019-12-02 17:16:06

Yes, special treatment. The JIT compiler is keenly aware of the way boxed value types work. Which is in general what makes value types acting a bit schizoid. Boxing involves creating a System.Object value that behaves exactly the same way as a value of a reference type. At that point, value type values no longer behave like values do at runtime. Which makes it possible, for example, to have a virtual method like ToString(). The boxed object has a method table pointer, just like reference types do.

The JIT compiler knows the method tables pointers for value types like int and bool up front. Boxing and unboxing for them is very efficient, it takes but a handful of machine code instructions. This needed to be efficient back in .NET 1.0 to make it competitive. A very important part of that is the restriction that a value type value can only be unboxed to the same type. This avoids the jitter from having to generate a massive switch statement that invokes the correct conversion code. All it has to do is to check the method table pointer in the object and verify that it is the expected type. And copy the value out of the object directly. Notable perhaps is that this restriction doesn't exist in VB.NET, its CType() operator does in fact generate code to a helper function that contains this big switch statement.

The problem with Enum types is that this cannot work. Enums can have a different GetUnderlyingType() type. In other words, the unboxed value has different sizes so simply copying the value out of the boxed object cannot work. Keenly aware, the jitter doesn't inline the unboxing code anymore, it generates a call to a helper function in the CLR.

That helper is named JIT_Unbox(), you can find its source code in the SSCLI20 source, clr/src/vm/jithelpers.cpp. You'll see it dealing with enum types specially. It is permissive, it allows unboxing from one enum type to another. But only if the underlying type is the same, you get an InvalidCastException if that's not the case.

Which is also the reason that Enum is declared as a class. Its logical behavior is of a reference type, derived enum types can be cast from one to another. With the above noted restriction on the underlying type compatibility. The values of an enum type have however very much the behavior of a value type value. They have copy semantics and boxing behavior.

Enums are specially dealt with by the CLR. If you want to go into the gory details, you can download the MS Partition II spec. In it, you'll find that Enums:

Enums obey additional restrictions beyond those on other value types. Enums shall contain only fields as members (they shall not even define type initializers or instance constructors); they shall not implement any interfaces; they shall have auto field layout (§10.1.2); they shall have exactly one instance field and it shall be of the underlying type of the enum; all other fields shall be static and literal (§16.1);

So that's how they can inherit from System.Enum, but have an "underlying" type - it's the single instance field they're allowed to have.

There is also a discussion on boxing behaviour, but it doesn't describe explicitly unboxing to the underlying type, that I can see.

Partition I, 8.5.2 states that enums are "an alternate name for an existing type" but "[f]or the purposes of matching signatures, an enum shall not be the same as the underlying type."

Partition II, 14.3 expounds: "For all other purposes, including verification and execution of code, an unboxed enum freely interconverts with its underlying type. Enums can be boxed to a corresponding boxed instance type, but this type is not the same as the boxed type of the underlying type, so boxing does not lose the original type of the enum."

Partition III, 4.32 explains the unboxing behavior: "The type of value type contained within obj must be assignment compatible with valuetype. [Note: This effects the behavior with enum types, see Partition II.14.3. end note]"

What I'm noting here is from page 38 of ECMA-335 (I suggest you download it just to have it):

The CTS supports an enum (also known as an enumeration type), an alternate name for an existing type. For the purposes of matching signatures, an enum shall not be the same as the underlying type. Instances of an enum, however, shall be assignable-to the underlying type, and vice versa. That is, no cast (see §8.3.3) or coercion (see §8.3.2) is required to convert from the enum to the underlying type, nor are they required from the underlying type to the enum. An enum is considerably more restricted than a true type, as follows:

The underlying type shall be a built-in integer type. Enums shall derive from System.Enum, hence they are value types. Like all value types, they shall be sealed (see §8.9.9).

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

This statement will be false because of the second sentence:

x is int

They are the same (an alias), but their signature is not the same. Converting to and from an int isn't a cast.

From page 46:

underlying types – in the CTS enumerations are alternate names for existing types (§8.5.2), termed their underlying type. Except for signature matching (§8.5.2) enumerations are treated as their underlying type. This subset is the set of storage types with the enumerations removed.

Go back to my Foo enum earlier. This statement will work:

Foo x = (Foo)5;

If you inspect the generated IL code of my Main method in 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 
}

Note there's no cast. ldc is found on page 86. It loads a constant. i4 is found on page 151, indicating the type is a 32-bit integer. There isn't a cast!

Extracted from MSDN:

The default underlying type of the enumeration elements is int. By default, the first enumerator has the value 0, and the value of each successive enumerator is increased by 1.

So, the cast is possible, but you need to force it:

The underlying type specifies how much storage is allocated for each enumerator. However, an explicit cast is needed to convert from enum type to an integral type.

When you box your enum into object, the animal object is derived from System.Enum (the real type is known at runtime) so it's actually an int, so the cast is valid.

  • (animal is Enum) returns true: For this reason you can unbox animal into an Enum or event into an int doing an explicit casting.
  • (animal is int) returns false: The is operator (in general type check) does not check the underlying type for Enums. Also, for this reason you need to do an explicit casting to convert Enum to int.

While enum types are inherited from System.Enum, any conversion between them is not direct, but a boxing/unboxing one. From C# 3.0 Specification:

An enumeration type is a distinct type with named constants. Every enumeration type has an underlying type, which must be byte, sbyte, short, ushort, int, uint, long or ulong. The set of values of the enumeration type is the same as the set of values of the underlying type. Values of the enumeration type are not restricted to the values of the named constants. Enumeration types are defined through enumeration declarations

So, while your Animal class is derived from System.Enum, it's actually an int. Btw, another strange thing is System.Enum is derived from System.ValueType, however it's still a reference type.

A Enum's underlying type is the type used to store the value of the constants. In your example, even though you haven't explicitly defined the values, C# does this:

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

Internally, Animal is made up of two constants with the integer values 0 and 1. That's why you can explicitly cast an integer to an Animal and an Animal to an integer. If you pass Animal.Dog to a parameter that accepts an Animal, what you are really doing is passing the 32bit integer value of Animal.Dog (in this case, 0). If you give Animal a new underlying type, then the values are stored as that type.

Why not... it is perfectly valid, for example, for a structure to hold an int internally, and be convertible to int with an explicit cast operator... lets simulate an 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();
    }
}

This struct can be used quite the same way a struct can... of course, if you try to reflect the type, and call IsEnum property it will return false.

Let's look at some usage comparison, with the equivalent enum:

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

Comparing usages:

Struct version:

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

Enum version:

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

Some operations cannot be simulated, but this all shows what I intended to say... Enums are special, yes... how much special? not that much! =)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!