编辑: :评论在底部。还, .


这就是让我感到困惑的东西。我的理解是,如果我有这样的枚举...

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

一般, 您不能从中解开一个值类型 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 提供了真正回答这个问题的参考。该解释可以在CLI规范的第II分区中找到:

出于绑定目的(例如,用于从用于调用它的方法参考的方法定义)枚举应与其基础类型不同。出于所有其他目的,包括验证和执行代码, 未包装的枚举与其基础类型自由互连. 。可以将枚举装箱到相应的盒装实例类型,但此类型为 不是 与基础类型的盒装类型相同,因此拳击不会丢失枚举的原始类型。

编辑(再次?!): :等等,实际上,我不知道我第一次读了这本书。也许这并不能100%解释专业的拆箱行为本身(尽管我将达米安的答案留在了接受的情况下,因为它对这个问题有了很大的了解)。我将继续研究这个...


另一个编辑: :伙计,然后 yodaj007的答案 扔了我一个循环。以某种方式枚举与 int;但是 int 可以分配给枚举变量 没有演员? buh?

我认为这一切最终都被 汉斯的答案, ,这就是为什么我接受它的原因。 (对不起,达米安!)

有帮助吗?

解决方案

是的,特殊待遇。 JIT编译器敏锐地意识到包装价值类型的工作方式。通常,这使价值类型的作用有点精神分裂。拳击涉及创建一个系统。对象值的行为与参考类型的值完全相同。到那时,值类型值不再像值在运行时一样行为。例如,这使得具有诸如ToString()之类的虚拟方法。盒装对象具有方法表指针,就像参考类型一样。

JIT编译器知道方法表的指示符,例如INT和BOOL的价值类型。拳击和拆箱是 非常 高效,只需少量机器代码说明。这需要在.NET 1.0中有效,以使其具有竞争力。一种 非常 重要的部分是,一个值类型值只能将其拆箱到同一类型的限制。这避免了抖动必须生成一个大规模的开关语句,该语句调用正确的转换代码。它要做的就是检查对象中的方法表指针,并验证它是预期的类型。并直接从对象中复制值。值得注意的是,在VB.NET中不存在此限制,其CTYPE()运算符实际上确实将代码生成了包含此大开关语句的助手函数。

枚举类型的问题在于,这无法正常工作。枚举可以具有不同的getunderlyingtype()类型。换句话说,未框的值具有不同的尺寸,因此简单地将盒装对象中的值复制不起。敏锐地意识到,抖动不再嵌入式拆箱代码,它会在CLR中生成对助手功能的调用。

该助手名为jit_unbox(),您可以在SSCLI20源中找到其源代码,CLR/src/vm/jithelpers.cpp。您会看到它专门处理枚举类型。它是允许的,它允许从一种枚举类型拆箱到另一种枚举类型。但是,只有在基础类型相同的情况下,如果事实并非如此。

这也是枚举被宣布为班级的原因。它的 逻辑 行为是一种参考类型,可以从一种到另一个人施放派生的枚举类型。上述对基础类型兼容性的限制。然而,枚举类型的值具有很大的价值类型值的行为。他们具有副本的语义和拳击行为。

其他提示

枚举是由CLR专门处理的。如果您想进入血腥的详细信息,可以下载 MS Partition II 规格在其中,您会发现枚举:

枚举遵守其他价值类型的额外限制。枚举应仅包含作为成员的字段(它们甚至不得定义类型的初始化器或实例构造函数);他们不得实施任何接口;他们应具有自动场布局(§10.1.2);他们应完全有一个实例字段,它应为枚举的基本类型;所有其他字段均应静态和字面意义(§16.1);

因此,这就是他们可以从system.enum继承的方式,但具有“基础”类型 - 这是他们允许拥有的单个实例字段。

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

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

分区II,14.3阐述:“出于所有其他目的,包括代码的验证和执行,未框的枚举与其基础类型自由互连。枚举可以盒装到相应的盒装实例类型,但此类型与盒子不同基础类型的类型,因此拳击不会丢失原始类型的枚举类型。”

分区III,4.32解释了拆箱行为: 值类型 包含在内 OBJ 必须与 值类型. 。 [注意:这会影响枚举类型的行为,请参见分区II.14.3。最终注释]”

我在这里注意到的是第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代码:

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

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

因此,演员是可能的,但是您需要强制它:

基础类型指定为每个枚举者分配多少存储。但是,需要明确的铸件将枚举类型转换为积分类型。

当您将枚举装进 object, ,动物物体源自 System.Enum (实际类型在运行时已知),所以它实际上是 int, ,所以演员阵容是有效的。

  • (animal is Enum) 返回 true: :因此,您可以将动物纳入枚举或事件中,以进行明确的铸造。
  • (animal is int) 返回 false: : 这 is 操作员(一般类型检查)未检查基础类型是否有枚举。另外,因此,您需要进行明确的铸造以将枚举转换为int。

而枚举类型是从 System.Enum, ,它们之间的任何转换不是直接的,而是拳击/拆箱。来自C#3.0规范:

枚举类型是具有命名常数的独特类型。每种枚举类型都有一种基础类型,必须是字节,sbyte,short,ushort,int,uint,long or long or ulong。枚举类型的值集与基础类型的一组值相同。枚举类型的值不限于命名常数的值。枚举类型是通过枚举声明定义的

因此,当您的动物课程源自 System.Enum, ,实际上是 int. 。顺便说一句,另一回事是 System.Enum 是从 System.ValueType, ,但是它仍然是一种参考类型。

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

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

在内部 Animal 由具有整数值0和1的两个常数组成 AnimalAnimal 到一个整数。如果您通过 Animal.Dog 到接受一个参数 Animal, ,您真正在做的是传递32位整数值 Animal.Dog (在这种情况下,0)。如果你给 Animal 新的基础类型,然后将值存储为该类型。

为什么不...

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;

某些操作无法模拟,但这一切都表明了我要说的话...枚举很特别,是的……多少特别?没有那么多! =)

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top