41

编辑:底部的评论。另外,.


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

enum Animal
{
    Dog,
    Cat
}

...我基本上所做的是定义了一个用两个定义的值调用的值类型,并且. 此类型派生自引用类型(值类型通常不能做的事情——至少在 C# 中是不允许的——但在这种情况下是允许的),并且具有来回转换到值/从值转换的功能。AnimalDogCat System.Enumint

如果我刚才描述的枚举类型是真的,那么我希望下面的代码会抛出一个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);

上述输出:

真的
错误的

MSDN 关于枚举的文档和 C# 规范都使用了术语“底层类型”;但我不知道这是什么意思,我也从未听说过它用于指代枚举以外的任何东西。“基础类型”实际上是什么意思


那么,这是否是另一个从 CLR 获得特殊处理的案例

我的钱是在这种情况下......但答案/解释会很好。


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

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

编辑(再次?!):等等,实际上,我不知道我第一次读对了。也许它并不能 100% 解释专门的拆箱行为本身(尽管我将 Damien 的回答视为已接受,因为它对这个问题有很大的了解)。我会继续研究这个...


另一个编辑:伙计,然后yodaj007 的回答让我陷入了另一个循环。不知何故,枚举与 ; 并不完全相同int。仍然int可以将 an 分配给没有强制转换的枚举变量吗?嗯?

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

4

8 回答 8

25

是的,特殊待遇。JIT 编译器敏锐地意识到装箱值类型的工作方式。这通常是使值类型表现得有点分裂的原因。装箱涉及创建一个 System.Object 值,该值的行为方式与引用类型的值完全相同。那时,值类型值的行为不再像运行时的​​值那样。例如,这使得拥有像 ToString() 这样的虚拟方法成为可能。装箱的对象有一个方法表指针,就像引用类型一样。

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

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

该助手名为 JIT_Unbox(),您可以在 SSCLI20 源代码 clr/src/vm/jithelpers.cpp 中找到它的源代码。您将看到它专门处理枚举类型。它是允许的,它允许从一种枚举类型拆箱到另一种枚举类型。但仅当基础类型相同时,如果不是这种情况,您会收到 InvalidCastException。

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

于 2011-01-07T16:01:15.873 回答
9

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

枚举遵循超出其他值类型的额外限制。枚举应仅包含作为成员的字段(它们甚至不应定义类型初始化器或实例构造器);他们不应实现任何接口;它们应具有自动字段布局(§10.1.2);它们应该只有一个实例字段,它应该是枚举的基础类型;所有其他字段应为静态和文字(第 16.1 节);

这就是他们可以从 System.Enum 继承的方式,但有一个“基础”类型——这是他们被允许拥有的单个实例字段。

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

于 2011-01-07T15:51:58.300 回答
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

它们相同的(别名),但它们的签名不同。与 an 相互转换int不是强制转换。

从第 46 页开始:

基础类型——在 CTS 中,枚举是现有类型的替代名称(第 8.5.2 节),称为它们的基础类型。除了签名匹配(第 8.5.2 节)之外,枚举被视为它们的基础类型。该子集是删除了枚举的存储类型集。

早点回到我的 Foo 枚举。该语句将起作用:

Foo x = (Foo)5;

如果您在 Reflector 中检查我的 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 页找到。它加载一个常量。 i4在第 151 页上找到,表明类型是 32 位整数。没有演员表!

于 2011-01-07T15:59:11.510 回答
4

第 I 部分 8.5.2 指出枚举是“现有类型的替代名称”,但“为了匹配签名的目的,枚举不应与基础类型相同”。

第二部分,14.3 阐述:“对于所有其他目的,包括验证和执行代码,未装箱的枚举与其基础类型自由相互转换。枚举可以装箱为相应的装箱实例类型,但这种类型与装箱的不同基础类型的类型,因此装箱不会丢失枚举的原始类型。”

Partition III, 4.32 解释了拆箱行为:“包含在obj中的值类型的类型必须与valuetype的赋值兼容。[注意:这会影响 enum 类型的行为,请参阅 Partition II.14.3. end note]”

于 2011-01-07T16:04:59.073 回答
3

摘自MSDN

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

所以,演员表是可能的,但你需要强制它:

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

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

  • (animal is Enum)Returns true:出于这个原因,您可以将 animal 拆箱为 Enum 或将 event 拆箱为 int 进行显式转换。
  • (animal is int)返回falseis运算符(一般类型检查)不检查 Enums 的基础类型。此外,出于这个原因,您需要进行显式转换以将 Enum 转换为 int。
于 2011-01-07T15:33:24.377 回答
2

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

枚举类型是具有命名常量的独特类型。每个枚举类型都有一个基础类型,它必须是 byte、sbyte、short、ushort、int、uint、long 或 ulong。枚举类型的值集与基础类型的值集相同。枚举类型的值不限于命名常量的值。枚举类型通过枚举声明定义

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

于 2011-01-07T15:36:42.520 回答
1

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

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

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

于 2011-01-07T15:53:13.313 回答
1

为什么不...这是完全有效的,例如,一个结构在内部保存一个 int,并可以通过显式转换运算符转换为 int...让我们模拟一个 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();
    }
}

该结构的使用方式与结构的使用方式完全相同……当然,如果您尝试反映类型并调用 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;

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

于 2014-02-28T16:22:16.293 回答