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

时间:2011-01-07 14:22:19

标签: .net inheritance enums clr unboxing

修改:底部的评论。另外,this


这就是让我困惑的事情。我的理解是,如果我有这样的枚举......

enum Animal
{
    Dog,
    Cat
}

...我基本上完成的是定义了一个名为Animal值类型,其中包含两个定义的值DogCat。此类型派生自引用类型 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);
    }
}

通常you cannot unbox a value type from System.Object as anything other than its exact type那么上述情况怎么样?就好像Animal类型 <{1}}(不仅仅是可转换int)而一个int(不仅仅是可兑换Enum)。 是多重继承吗? Enum是否以某种方式继承自System.Enum(我不希望这样的事情)?

修改:它不能是上述任何一个。以下代码最终证明了这一点(我认为):

System.Int32

以上输出:

True
False

the MSDN documentation on enumerations和C#规范都使用术语“底层类型”;但是我不知道这意味着什么,也没有听过它用于引用除枚举之外的任何东西。 “底层类型”实际上是什么意思


那么,这是另一个case that gets special treatment from the CLR吗?

我的钱就是这样......但答案/解释会很好。


更新Damien_The_Unbeliever提供了真正回答此问题的参考。可以在CLI规范的分区II中的枚举部分中找到解释:

  

用于绑定目的(例如,用于   从中定位方法定义   用于调用它的方法引用)   枚举应与其不同   基础类型。对于所有其他   目的,包括验证和   执行代码,未装箱的枚举   与它自由地互相转换   基础类型。枚举可以装箱   到相应的盒装实例   类型,但此类型相同   作为底层的盒装类型   类型,所以拳击不会失去   枚举的原始类型。

编辑(再次?!):等等,实际上,我不知道我第一次读到这是对的。也许它不能100%解释专门的拆箱行为本身(虽然我将Damien的答案视为已被接受,因为它在这个问题上有很多亮点)。我会继续研究这个......


另一个编辑:男子,然后yodaj007's answer把我扔了另一个循环。不知何故,枚举与object animal = Animal.Dog; Console.WriteLine(animal is Enum); Console.WriteLine(animal is int); 不完全相同;但是int可以分配给枚举变量而没有强制转换?乌兰布?

我认为这最终都被Hans's answer所照亮,这就是我接受它的原因。 (抱歉,Damien!)

8 个答案:

答案 0 :(得分:23)

是的,特殊待遇。 JIT编译器非常清楚盒装值类型的工作方式。这通常是什么使得价值类型表现得有点精神错乱。拳击涉及创建一个System.Object值,其行为与引用类型的值完全相同。此时,值类型值不再像运行时的​​值那样运行。例如,这使得有可能拥有像ToString()这样的虚方法。盒装对象有一个方法表指针,就像引用类型一样。

JIT编译器知道方法表指针的值类型,如int和bool。为他们装箱和取消装箱是非常高效,它只需要一些机器代码指令。这需要在.NET 1.0中高效,以使其具有竞争力。 非常的重要部分是限制值,即值类型值只能拆分为相同类型。这避免了抖动必须生成调用正确转换代码的大量switch语句。它所要做的就是检查对象中的方法表指针并验证它是否是预期的类型。并直接从对象中复制值。值得注意的是,这种限制在VB.NET中不存在,其CType()运算符确实为包含这个大开关语句的辅助函数生成代码。

Enum类型的问题是这不起作用。枚举可以具有不同的GetUnderlyingType()类型。换句话说,未装箱的值具有不同的大小,因此简单地将值从盒装对象中复制不起作用。敏锐地意识到,抖动不再内联拆箱代码,它会在CLR中生成对辅助函数的调用。

该帮助程序名为JIT_Unbox(),您可以在SSCLI20源代码clr / src / vm / jithelpers.cpp中找到它的源代码。你会看到它专门处理枚举类型。这是允许的,它允许从一个枚举类型到另一个枚举类型的拆箱。但是,只有当底层类型相同时,如果情况并非如此,则会出现InvalidCastException。

这也是将Enum声明为类的原因。它的逻辑行为属于引用类型,派生的枚举类型可以从一个转换为另一个。具有上述对底层类型兼容性的限制。然而,枚举类型的值具有值类型值的行为。他们有复制语义和拳击行为。

答案 1 :(得分:9)

Enum由CLR专门处理。如果您想了解血腥细节,可以下载MS Partition II规范。在其中,你会发现Enums:

  

Enums遵守其他限制   超越其他价值类型。   枚举应仅包含字段   成员(他们甚至不会定义   类型初始值设定项或实例   构造函数);他们不应该   实现任何接口;他们   应具有自动场布局   (§10.1.2);他们应该只有一个   实例字段,它应该是基础类型   enum;所有其他领域都应该是   static and literal(§16.1);

这就是他们如何从System.Enum继承,但有一个“底层”类型 - 它是允许的单实例字段。

还有关于拳击行为的讨论,但它没有明确地描述基础类型的拆箱,我可以看到。

答案 2 :(得分:4)

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

  

CTS支持枚举(也称为枚举类型),即现有类型的备用名称。出于匹配签名的目的,枚举不应与基础类型相同。然而,枚举的实例应该可分配给基础类型,反之亦然。也就是说,不需要强制转换(参见§8.3.3)或强制(参见§8.3.2)从枚举转换为基础类型,也不需要从基础类型到枚举。枚举比真实类型更受限制,如下所示:

     

底层类型应为内置整数类型。枚举应从System.Enum派生,因此它们是值类型。与所有价值类型一样,它们应密封(见§8.9.9)。

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

由于第二句话,这句话将是假的:

x is int

他们 相同(别名),但他们的签名不一样。转换为int和不转换为Foo x = (Foo)5;

从第46页开始:

  

底层类型 - 在CTS枚举中是现有类型的备用名称(第8.5.2节),称为其基础类型。除签名匹配(第8.5.2节)外,枚举被视为其基础类型。此子集是已删除枚举的存储类型集。

早些时候回到我的Foo enum。该声明将起作用:

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

如果在Reflector中检查生成的Main方法的IL代码:

ldc

注意没有演员表。 {86}找到i4。它加载常量。第149页上的{{1}}表示类型是32位整数。没有演员!

答案 3 :(得分:4)

分区I,8.5.2规定枚举是“现有类型的替代名称”,但“[f]或匹配签名的目的,枚举不应与基础类型相同。”

分区II,14.3阐述:“出于所有其他目的,包括验证和执行代码,未装箱的枚举与其基础类型自由地互换。枚举可以装箱到相应的盒装实例类型,但这种类型不一样作为底层类型的盒装类型,因此装箱不会丢失枚举的原始类型。“

分区III,4.32解释了取消装箱行为:“ obj 中包含的值类型的类型必须与 valuetype 兼容。[注意:这会影响枚举类型的行为,请参阅分区II.14.3。结束注释]“

答案 4 :(得分:3)

摘自MSDN

  

枚举元素的默认基础类型是int。默认情况下,第一个枚举数的值为0,每个连续枚举数的值增加1。

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

  

基础类型指定为每个枚举器分配多少存储空间。但是,需要显式转换才能从枚举类型转换为整数类型。

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

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

答案 5 :(得分:2)

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

  

枚举类型是一种不同的类型   使用命名常量。一切   枚举类型有底层   type,必须是byte,sbyte,   简而言之,ushort,int,uint,long或   ULONG。一组价值观   枚举类型与   基础类型的值集。   枚举类型的值不是   限制为命名的值   常量。枚举类型是   通过枚举定义   声明

因此,虽然您的Animal类派生自System.Enum,但它实际上是int。顺便说一句,另一个奇怪的事情System.Enum来自System.ValueType,但它仍然是一种参考类型。

答案 6 :(得分:1)

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

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

在内部,Animal由两个常量组成,整数值为0和1.这就是为什么你可以显式地将整数转换为AnimalAnimal整数。如果将Animal.Dog传递给接受Animal的参数,那么您实际所做的是传递32位整数值Animal.Dog(在本例中为0)。如果为Animal提供新的基础类型,则值将存储为该类型。

答案 7 :(得分:1)

为什么不...例如,它是完全有效的,例如,一个结构在内部持有一个int,并且可以通过一个显式的强制转换操作符转换为int ...让我们模拟一个枚举:

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;

枚举版:

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

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