关于.net:枚举是如何从System.Enum派生出来的,同时又是一个整数?

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

编辑:底部的注释。还有,这个。

这就是让我困惑的地方。我的理解是如果我有这样的枚举…

1
2
3
4
5
enum Animal
{
    Dog,
    Cat
}

…我基本上做的是定义一个名为Animal的值类型,有两个定义的值,DogCat。此类型派生自引用类型System.Enum(值类型通常至少不能在c中执行,但在这种情况下是允许的),并且具有向/从int值来回转换的功能。

如果我刚才描述的枚举类型是正确的,那么我希望下面的代码抛出一个InvalidCastException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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继承(我不希望是可能的)?< /打击>

编辑:不能是上面的任何一个。以下代码(我认为)最终证明了这一点:

1
2
3
4
object animal = Animal.Dog;

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

以上输出:

1
2
True
False

关于枚举的msdn文档和c规范都使用了术语"基础类型";但我不知道这意味着什么,也没有听说过它用于除枚举以外的任何其他方面。"基础类型"实际上是什么意思?

那么,这是另一个从clr得到特殊治疗的案例吗?

我的钱就在这件事上…但回答/解释会很好。

最新消息:不信者达米安提供了真实回答这个问题的参考。解释可以在cli规范的分区II中的枚举部分找到:

For binding purposes (e.g., for
locating a method definition from the
method reference used to call it)
enums shall be distinct from their
underlying type. 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.

再次编辑?!):等等,事实上,我不知道我第一次读对了。也许这并不能100%地解释这种特殊的拆箱行为本身(尽管我将达米恩的回答保留为可接受的,因为它对这个问题有很大的启发)。我会继续调查这个…

另一个编辑:伙计,然后尤达J007的回答又让我陷入了另一个困境。不知何故,枚举与int并不完全相同;然而,可以将int分配给不带强制转换的枚举变量?嘘?

我认为汉斯的回答最终解释了这一切,这就是为什么我接受了它。(对不起,达米恩!)


是的,特别治疗。JIT编译器非常清楚装箱值类型的工作方式。一般来说,这使得值类型有点分裂。装箱涉及创建与引用类型的值行为完全相同的System.Object值。此时,值类型值的行为不再像运行时的值那样。例如,这使得有一个类似toString()的虚拟方法成为可能。装箱对象有一个方法表指针,就像引用类型一样。

JIT编译器知道前面int和bool等值类型的方法表指针。装箱和拆箱对于他们是非常有效的,它只需要少量的机器代码指令。这需要在.NET 1.0中保持高效才能使其具有竞争力。其中一个非常重要的部分是限制值类型值只能取消绑定到同一类型。这就避免了必须生成调用正确转换代码的大规模switch语句而产生的抖动。它所要做的就是检查对象中的方法表指针,并验证它是否是所需的类型。直接从对象中复制值。值得注意的是,这个限制在vb.net中并不存在,它的ctype()运算符实际上会为包含这个大switch语句的助手函数生成代码。

枚举类型的问题是它不能工作。枚举可以具有不同的getUnderlyingType()类型。换句话说,未装箱的值大小不同,因此简单地从装箱的对象中复制值是行不通的。敏锐地意识到,抖动不再嵌入未绑定代码,它会生成对clr中助手函数的调用。

这个助手名为jit_unbox(),您可以在sscli20源clr/src/vm/jithelpers.cpp中找到它的源代码。您将看到它专门处理枚举类型。它是允许的,它允许从一个枚举类型取消绑定到另一个枚举类型。但是,只有在基础类型相同的情况下,如果情况并非如此,则会得到一个invalidCastException。

这也是枚举声明为类的原因。它的逻辑行为是引用类型,派生枚举类型可以从一个类型强制转换到另一个类型。上面提到的对底层类型兼容性的限制。然而,枚举类型的值具有非常多的值类型值的行为。它们具有复制语义和装箱行为。


枚举是由clr专门处理的。如果您想了解详细信息,可以下载MS分区II规范。在其中,您会发现该枚举:

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

所以这就是它们可以从System.Enum继承的方式,但是有一个"底层"类型——这是允许它们拥有的单个实例字段。

还有一个关于拳击行为的讨论,但是它没有明确地描述取消绑定到底层类型,我可以看到。


分区I,8.5.2规定枚举是"现有类型的备用名称",但"[f]或为了匹配签名,枚举不应与基础类型相同。"

分区II,14.3阐述了:"对于所有其他目的,包括验证和执行代码,一个未装箱的枚举可以自由地与其基础类型进行交互。枚举可以装箱到相应的装箱实例类型,但此类型与基础类型的装箱类型不同,因此装箱不会丢失枚举的原始类型。"

第三部分,4.32解释了拆箱行为:"obj中包含的值类型的类型必须与赋值兼容。[注:这会影响枚举类型的行为,请参见第II.14.3节。结束笔记


我在这里注意到的是来自ECMA-335第38页(我建议您下载它只是为了拥有它):

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).

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

由于第二句话,本声明将是错误的:

1
x is int

它们是相同的(别名),但它们的签名不同。转换成和转换成int不是一个演员表。

第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.

早点回到我的foo枚举。此声明将有效:

1
Foo x = (Foo)5;

如果您在Reflector中检查我的主要方法生成的IL代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
.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位整数。没有演员!


从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.

所以,演员阵容是有可能的,但你需要强迫它:

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.

当您将枚举装箱到object中时,动物对象是从System.Enum派生的(实际类型在运行时已知),因此它实际上是int,因此强制转换是有效的。

  • (animal is Enum)返回true:因此,可以将动物取消装箱为枚举或事件,并将其放入int中,执行显式转换。
  • (animal is int)返回falseis运算符(在一般类型检查中)不检查枚举的基础类型。此外,由于这个原因,您需要执行显式转换以将枚举转换为int。


虽然枚举类型是从System.Enum继承的,但它们之间的任何转换都不是直接的,而是装箱/拆箱转换。从C 3.0规范:

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

所以,虽然你的动物类来源于System.Enum,但实际上它是int。顺便说一下,另一个奇怪的事情是System.Enum是从System.ValueType派生出来的,但是它仍然是一个引用类型。


为什么不。。。它是完全有效的,例如,对于一个内部保存一个int的结构,并且可以用一个显式的强制转换运算符转换为int…让我们模拟枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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。

让我们看一些使用比较,使用等价的枚举:

1
2
3
4
5
6
enum MyEnum
{
    EnumItem1 = 0,
    EnumItem2 = 2,
    EnumItem3 = 10,
}

比较用法:

结构版本:

1
2
3
4
5
6
var val = MyEnum.EnumItem1;
val = (MyEnum)50;
val = 0;
object obj = val;
bool isE = obj is MyEnum;
Enum en = val;

枚举版本:

1
2
3
4
5
6
var valS = MyEnumS.EnumItem1;
valS = (MyEnumS)50;
//valS = 0; // cannot simulate this
object objS = valS;
bool isS = objS is MyEnumS;
IEnum enS = valS;

有些操作无法模拟,但这一切都显示了我想说的……枚举是特殊的,是的…有多特别?没那么多!=)


Enum的基础类型是用于存储常量值的类型。在您的示例中,即使您没有明确定义值,C也会这样做:

1
2
3
4
5
enum Animal : int
{
    Dog = 0,
    Cat = 1
}

在内部,Animal由两个常量组成,其整数值为0和1。这就是为什么可以显式地将一个整数强制转换为一个Animal,将一个Animal强制转换为一个整数。如果您将Animal.Dog传递给接受Animal的参数,那么您真正要做的是传递Animal.Dog的32位整数值(在本例中为0)。如果给Animal一个新的基础类型,那么这些值将存储为该类型。