它是如何从一个枚举派生System.Enum是一个整数在同一时间?
编辑 :底部的评论。 另外, 这个 。
这是什么让我困惑。 我的理解是,如果我有这样的枚举…
enum Animal { Dog, Cat }
…我基本上做了什么是定义了一个值types称为Animal
与两个定义值, Dog
和Cat
。 这种types派生自引用types System.Enum
(通常情况下,值types通常不会这样做 – 至less在C#中是不允许的,但在这种情况下允许这样做),并且具有用于来回转换int
值的工具。
如果我刚刚描述的枚举types的方式是真实的,那么我期望下面的代码抛出一个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
的值types取消为除确切types之外的其他值 。 那么上述可能如何呢? 这就好像Animal
types是一个int
(不只是可转换为int
),同时也是一个Enum
(不仅可以转换为Enum
)。 是多重inheritance吗? System.Enum
以某种方式inheritanceSystem.Int32
(我不希望有可能)?
编辑 :它不能是以上任一。 下面的代码最终certificate了这一点(我认为):
object animal = Animal.Dog; Console.WriteLine(animal is Enum); Console.WriteLine(animal is int);
以上输出:
真正 假
枚举的MSDN文档和C#规范都使用术语“基础types”; 但是我不知道这是什么意思,也没有听说过它是用来引用除枚举之外的任何东西。 “底层types”究竟是什么意思 ?
那么,这又是一个从CLR获得特殊待遇的案例吗?
我的钱就是这样…但答案/解释会很好。
更新 : Damien_The_Unbeliever提供了真正回答这个问题的参考。 有关说明可以在CLI规范的分区II中的enums部分find:
为了约束的目的(例如,从用于调用它的方法引用来定位方法定义),枚举应该与它们的基础types不同。 出于所有其他目的,包括validation和执行代码, 一个unboxed枚举自由地与其基础types相互转换 。 枚举可以被装箱到相应的盒装实例types,但是这种types与底层types的盒装types不同,所以装箱不会丢失枚举的原始types。
编辑(再次?!) :等等,实际上,我不知道我第一次阅读正确。 也许它不是100%解释专门的拆箱行为本身(虽然我离开达米安的答案被接受,因为它在这个问题上大量的光)。 我会继续研究这个…
另一个编辑 :人,然后yodaj007的答案扔我另一个循环。 不知怎的,一个枚举不完全一样int
; 但一个int
可以分配给一个枚举variables没有转换 ? 寮步?
我认为这一切都最终都是汉斯的回答 ,这就是为什么我接受了它。 (对不起,达米安!)
是的,特殊待遇 JIT编译器非常了解盒装值types的工作方式。 一般来说,价值types有点分裂。 拳击包括创build一个System.Object值,其行为与引用types的值完全相同。 此时,值types值不再像运行时的值一样。 这使得例如有一个像ToString()这样的虚拟方法成为可能。 盒装对象有一个方法表指针,就像引用types一样。
JIT编译器知道像int和bool这样的值types的方法表指针。 拳击和拆箱对他们来说是非常有效的,它只需要less量的机器代码指令。 这需要在.NET 1.0中高效地使其具有竞争力。 其中一个非常重要的部分是限制,值types值只能被拆箱到相同的types。 这样可以避免抖动产生一个调用正确转换代码的大量switch语句。 它所要做的就是检查对象中的方法表指针,并确认它是预期的types。 并直接复制对象的值。 值得注意的可能是这个限制在VB.NET中不存在,它的CType()运算符实际上是生成代码给包含这个大开关语句的帮助函数。
枚举types的问题是,这不能工作。 枚举可以有一个不同的GetUnderlyingType()types。 换句话说,unboxed的值有不同的大小,所以简单地把值从盒装对象中复制出来是行不通的。 敏锐地意识到,抖动不再内联取消装箱代码,它会产生一个调用CLR中的帮助函数。
该帮助程序名为JIT_Unbox(),您可以在SSCLI20源代码clr / src / vm / jithelpers.cpp中find其源代码。 你会看到它专门处理枚举types。 它是宽容的,它允许从一个枚举types拆箱到另一个。 但是只有底层types相同,如果不是这样的话,你会得到一个InvalidCastException。
这也是Enum被宣布为一个类的原因。 它的逻辑行为是一个引用types,派生的枚举types可以从一个到另一个。 有了上述对底层types兼容性的限制。 然而,枚举types的值具有非常多的值types值的行为。 他们有复制语义和装箱行为。
枚举由CLR特别处理。 如果你想进入血淋淋的细节,你可以下载MS分区II规范。 其中,你会发现枚举:
除了其他值types之外,枚举符合其他限制。 枚举应只包含字段作为成员(他们甚至不应该定义types初始值设定项或实例构造函数); 他们不应该实现任何接口; 他们应该有自动场布局(§10.1.2); 它们应该只有一个实例字段,并且应该是枚举的基本types; 所有其他领域应是静态的和字面的(§16.1);
所以这就是他们如何从System.Enuminheritance,但有一个“基础”types – 这是他们被允许具有的单个实例字段。
还有关于拳击行为的讨论,但是它没有明确地描述拆箱到底层的types,我可以看到。
分区I,8.5.2指出,枚举是“现有types的替代名称”,但是“或者匹配签名的目的,枚举不应与基础types相同”。
分区II,14.3阐述:“对于所有其他目的,包括validation和执行代码,一个unboxed枚举可以自由地与其基础types相互转换。枚举可以被装箱到相应的盒装实例types,但是这种types与盒装底层types的types,所以拳击不会丢失枚举的原始types。“
分区III,4.32解释了拆箱行为:“ obj中包含的值types的types必须赋值与valuetype兼容[注意:这会影响枚举types的行为,参见分区II.14.3。
我在这里注意的是从ECMA-335第38页(我build议你下载它只是为了它):
CTS支持枚举(也称为枚举types),即现有types的替代名称。 为了匹配签名的目的,枚举不应该与基础types相同。 但是,枚举的实例应该是可分配的 – 对于基础types,反之亦然。 也就是说,不需要强制转换(请参阅第8.3.3节)或强制转换(请参阅第8.3.2节)将枚举types转换为基础types,也不需要从基础types转换为枚举types。 一个枚举比一个真正的types有更多的限制,如下所示:
基础types应该是一个内置的整数types。 枚举应从System.Enum派生,因此它们是值types。 像所有的值types一样,它们应该被密封(见§8.9.9)。
enum Foo { Bar = 1 } Foo x = Foo.Bar;
由于第二句话,这个陈述将是错误的:
x is int
他们是相同的(别名),但他们的签名是不一样的。 转换成int
并不是一个强制转换。
从第46页开始:
基础types – 在CTS枚举中是现有types的替代名称(第8.5.2节),称为基础types。 除了签名匹配(第8.5.2节),枚举被视为其基础types。 这个子集是删除了枚举的存储types集合。
早些回到我的Foo enum。 这个声明将起作用:
Foo x = (Foo)5;
如果您检查reflection器中的Main方法生成的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页find。它加载一个常量。 在页151findi4
,指示types是一个32位整数。 没有演员!
从MSDN提取:
枚举元素的默认底层types是int。 默认情况下,第一个枚举数的值为0,每个连续的枚举数的值增加1。
所以,演员是可能的,但你需要强制:
基础types指定为每个枚举器分配多less存储空间。 但是,需要显式强制转换才能从枚举types转换为整型。
当你将enum放入object
,动物对象是从System.Enum
派生的(实际types在运行时是已知的),所以它实际上是一个int
,所以这个转换是有效的。
-
(animal is Enum)
返回true
:由于这个原因,你可以解除动物进入一个inttypes的Enum或事件,做一个明确的铸造。 -
(animal is int)
返回false
:is
运算符(一般types检查)不检查枚举的基础types。 此外,由于这个原因,你需要做一个明确的转换来将Enum转换为int。
虽然枚举types是从System.Enum
inheritance的,但它们之间的任何转换都不是直接的,而是装箱/拆箱。 来自C#3.0规范:
枚举types是具有命名常量的独特types。 每个枚举types都有一个基础types,它必须是字节,sbyte,short,ushort,int,uint,long或ulong。 枚举types的值的集合与基础types的值的集合相同。 枚举types的值不限于指定常量的值。 枚举types是通过枚举声明定义的
所以,虽然你的Animal类是从System.Enum
派生的,但它实际上是一个int
。 顺便说一句,另一个奇怪的是System.Enum
派生自System.ValueType
,但它仍然是一个引用types。
Enum
的基础types是用于存储常量值的types。 在您的示例中,即使您没有明确定义值,C#也会这样做:
enum Animal : int { Dog = 0, Cat = 1 }
在内部, Animal
由两个常量组成,整数值为0和1.这就是为什么您可以明确地将一个整数转换为一个Animal
和一个Animal
整数。 如果您将Animal.Dog
传递给接受Animal
的参数,那么您实际上正在传递Animal.Dog
的32位整数值(在本例中为0)。 如果您给Animal
一个新的基础types,那么这些值将被存储为该types。
为什么不呢……这是完全有效的,例如,一个结构内部保存一个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(); } }
这个结构可以用结构相同的方式…当然,如果你试图反映types,并调用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;
有些操作是不能模拟的,但是这一切都显示了我想说的…枚举是特殊的,是的…有多特别? 没有那么多! =)