现在的位置: 首页 > 综合 > 正文

关于枚举的种种 (Enumeration FAQ) [C#, IL, BCL]

2012年10月14日 ⁄ 综合 ⁄ 共 7374字 ⁄ 字号 评论关闭

关于枚举的种种 [C#, IL, BCL]

Enumeration FAQ [C#, IL, BCL]

 

Updated on Tuesday, January 11, 2005

 

Written by Allen Lee

 

Q:在C#里,我们如何表达枚举类型?

A:你可以使用enum关键字(keyword)来声明一个枚举类型(enum type):

  // Code #01
  public enum Alignment
  
{
      Left,
      Center,
      Right
  }

Q:枚举类型是值类型(value type)还是引用类型(reference type)?

A:枚举类型都是值类型。


Q:System.Enum是枚举类型么?

A:不是。


Q:System.Enum与枚举类型(enum type)有什么关系?

A:System.Enum是一个抽象类(abstract class),所有枚举类型都直接继承自它,当然也同时继承了它的所有成员。


Q:那么System.Enum属于引用类型啦?

A:是的。


Q:既然System.Enum是引用类型,而枚举类型又是直接继承自System.Enum的,那为什么枚举类型却不是引用类型?

A:这种继承关系是隐式的并由编译器负责展开,上面Code #1的Alignment枚举被展开后的IL代码如下:

  // Code #02
  .class public auto ansi sealed Aligment
         extends [mscorlib]System.Enum
  
{
      .field 
public static literal Aligment Left = int32(0x00000000)
      .field 
public static literal Aligment Center = int32(0x00000001)
      .field 
public static literal Aligment Right = int32(0x00000002)

      .field 
public specialname rtspecialname int32 value__
  }

从声明中,你可以看到Aligment的确是继承自System.Enum的,只是你不能在C#里显式声明这种继承关系。


Q:但你好像没有回答为什么枚举类型继承自一个引用类型后,却还是值类型!

A:你知道,所有的值类型都是System.ValueType的后代,枚举类型也不例外,枚举类型直接继承自System.Enum,而System.Enum却又直接继承自System.ValueType的,所以,枚举类型也是System.ValueType的后代。


Q:慢着!从System.ValueType派生出来的类型不都应该是值类型吗?为什么System.Enum会是引用类型?

A:正确的说法应该是“值类型都是System.ValueType的后代”,但System.ValueType的后代不全是值类型,System.Enum就是唯一的特例!在System.ValueType的所有后代中,除了System.Enum之外其它都是值类型。事实上,我们可以在.NET的源代码中找到System.Enum的声明:

public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible

请注意,.NET Framework SDK v2.0.3600.0 Documentation中的Enum声明是错的:

public abstract struct Enum : IComparable, IFormattable, IConvertible


Q:开始头晕了,究竟枚举类型、System.Enum、System.ValueType、值类型和引用类型之间存在着什么样的关系?

A:简单的说,

  • 1. 所有枚举类型(enum type)都是值类型。
  • 2. System.Enum和System.ValueType本身是引用类型。
  • 3. 枚举类型(enum type)都是隐式的直接继承自System.Enum,并且这种继承关系只能由编译器自动展开。但System.Enum本身不是枚举类型(enum type)。
  • 4. System.Enum是一个特例,它直接继承自System.ValueType(参见Code #03),但本身却是一个引用类型。

好吧,现在来看看下面代码,你能猜得出它的输出结果吗?

  // Code #04
  static void Main()
  
{
      Type t 
= typeof(System.Enum);

      
if (t.IsEnum)
          Console.WriteLine(
"I'm enum type.");

      
if (t.IsValueType)
          Console.WriteLine(
"I'm value type.");
  }

请别惊讶于程序的运行结果没有任何输出!对于第一个判断,我们很清楚System.Enum并不是枚举类型。但第二个判断呢?System.Enum明明继承自System.ValueType,却不承认是System.ValueType的后代!这是.NET上的一个特例,恰恰体现出System.Enum是特殊性。


Q:既然枚举类型是值类型,自然会涉及到装箱和拆箱(boxing and unboxing)的问题,那么枚举类型会被装箱成什么呢?[Updated]

A:枚举类型可以被装箱成System.Enum、System.ValueType、System.Object或者System.IConvertible、System.IFormattable、System.IComparable。

注意:在.NET 1.1上,枚举类型只能被装箱到System.Enum、System.ValueType、System.Object;而在.NET 2.0上,枚举类型还能被装箱到System.Enum所实现的三个接口:System.IConvertible、System.IComparable、System.IFormattable。对应的装箱操作既可以为隐式的也可以是显式的。

下面的C#代码:

  // Code #05
  
// See Code #01 for Alignment.
  static void Main()
  
{
      Alignment a 
= Alignment.Center;

      Console.WriteLine(a.ToString());

      Console.WriteLine(a);
  }

对应的IL代码是:

  // Code #06
  .method private hidebysig static void Main() cil managed
  
{
      
.entrypoint
      
// Code Size: 32 byte(s)
      .maxstack 1
      
.locals (
            EnumerationFaq.Alignment alignment1)
      L_0000: ldc.i4.1 
      L_0001: stloc.0 
      L_0002: ldloc.0 
      L_0003: box EnumerationFaq.Alignment
      L_0008: call instance 
string [mscorlib]System.Enum::ToString()
      L_000d: call 
void [mscorlib]System.Console::WriteLine(string)
      L_0012: nop 
      L_0013: ldloc.0 
      L_0014: box EnumerationFaq.Alignment
      L_0019: call 
void [mscorlib]System.Console::WriteLine(object)
      L_001e: nop 
      L_001f: ret 
  }

从IL代码中我们可以看到枚举类型被装箱两次。第一次(L_0003)被装箱成System.Enum,而第二次(L_0014)就被装箱成System.Object。

但如果你让编译器自动为你选择装箱类型的话,它会优先考虑System.Enum:

  // Code #07
  
// See Code #01 for Alignment.
  class Program
  
{
      
static void Main()
      
{
          Alignment a 
= Alignment.Center;

          Print(a);
      }


      
static void Print(IConvertible c)
      
{
          Console.WriteLine(c);
      }


      
static void Print(IFormattable f)
      
{
          Console.WriteLine(f);
      }


      
static void Print(IComparable c)
      
{
          Console.WriteLine(c);
      }


      
static void Print(Object o)
      
{
          Console.WriteLine(o);
      }


      
static void Print(ValueType v)
      
{
          Console.WriteLine(v);
      }


      
static void Print(Enum e)
      
{
          Console.WriteLine(e);
      }

  }

上面的代码将被编译成如下的IL:

  // Code #08
  .method private hidebysig static void Main(string[] args) cil managed
  
{
      .entrypoint
      
// Code Size: 15 byte(s)
      .maxstack 1
      .locals (
            EnumerationFaq.Alignment alignment1)
      L_0000: ldc.i4.
1 
      L_0001: stloc.
0 
      L_0002: ldloc.
0 
      L_0003: box EnumerationFaq.Alignment
      
// 调用static void Print(Enum e);
      L_0008: call void EnumerationFaq.Program::Print([mscorlib]System.Enum)
      L_000d: nop 
      L_000e: ret 
  }

Q:我留意到Code #02中的

.field public static literal Aligment Center = int32(0x00000001)

该语句明显是整数赋值,这是否说明枚举类型实质上是整数类型?

A:这说明枚举类型与整数类型的确有一定的关系。事实上,每一个枚举类型都有与之相对应的整数类型,我们称该整数类型为底层类型(underlying type),默认的情况下使用,.NET使用System.Int32。当然,你可以手动将其指定为其他的整数类型:

  // Code #09
  public enum Alignment : byte
  
{
      Left,
      Center,
      Right
  }

注意,能被指定为枚举的底层类型的只能是如下所列的整数类型:byte, sbyte, short, ushort, int, uint, long, ulong


Q:为何我们需要指定枚举类型的底层类型?

A:你完全可以让它接受默认的底层类型。请留意Code #08,你完全找不到“Center”这个字眼,然而在C#代码中,它却是存在的,为什么呢?这是因为代码在编译的时候,编译器把枚举类型转换为与之对应的底层类型的数值来处理。Code #08的L_0000实际上就是把类型为System.Int32的数值1推入堆栈,而不是把“Center”推入堆栈。事实上,底层类型说明了如何为枚举类型分配空间,不同的底层类型所占用的资源不同,大概当你在受限系统上进行开发的话,你就可能需要注意一下了。


Q:枚举成员的值是怎样规定的?

A:如果你没有手动指定成员的值的话,从上往下看,各成员的值为:0, 1, 2, ...。说罢了,就是一个非负整数等差数列,其初值为0,步长为1。例如:

  // Code #10
  public enum Alignment
  
{
      Left,    
// 0
      Center,    // 1
      Right    // 2
  }

Q:如果我有手动指定某些成员的值呢?

A:那么被赋值的成员的值就是你所指定的值。当然,无论你是否手动指定枚举成员的值,递增步长都不会变,总是为1。为了测试你是否理解,请说出下面枚举个成员的值以及你的判断理由(请用人脑而不是电脑来运行以下代码):

  // Code #11
  public enum DriveType : sbyte
  
{
      CDRom,
      Fixed 
= -2,
      Network,
      NoRootDirectory 
= -1,
      Ram,
      Removable 
= Network * NoRootDirectory,
      Unknown
  }

Q:我们如何获取枚举成员的值,无论成员是否被手动赋值?

A:你可以使用System.Enum的

public static Array GetValues(Type enumType);

该方法返回一个包含所有枚举成员的数组:

  // Code #12
  
// See Code #01 for Alignment.
  public static void Main()
  
{
      Alignment[] alignments 
= (Alignment[])Enum.GetValues(typeof(Alignment));
      Console.WriteLine(
"Wanna see the values of Alignment's menbers?");
      
foreach (Alignment a in alignments)
          Console.WriteLine(
"{0:G} = {0:D}", a);
  }


  
// Output:
  
// Wanna see the values of Alignment's menbers?
  
// Left = 0
  
// Center = 1
  
// Right = 2

Q:如果我只需要其中某些枚举成员的值呢?

A:那么你可以把枚举转换为IConvertible接口,再调用对应的方法:

  // Code #12
  
// See Code #01 for Alignment.
  public static void Main()
  
{
      IConvertible ic 
= (IConvertible)Alignment.Center;
      
int i = ic.ToInt32(null);
      Console.WriteLine(
"The value of Alignment.Center is {0}.", i);
  }


  
// Output:
  
// The value of Alignment.Center is 1.

Q:为什么需要手动指定枚举成员的值?

A:一般情况下,使用默认的赋值规则就足够了,但某些情况下,为枚举成员指定一个与实际情况(模型)相符的值可能更有意义,这要视你具体所建的模型而定。

还是让我们来一个实际的例子:

  // Code #13
  public enum CustomerKind
  
{
      Normal 
= 90,
      Vip 
= 80,
      SuperVip 
= 70,
      InActive 
= 100
  }


  
public class Customer
  
{
      
public readonly CustomerKind Kind;

      
private double m_Payment;
      
public double Payment
      
{
          
return m_Payment * (int)Kind / 100;
      }


      
// Code here
  }

我为枚举CustomerKind的每个成员都赋了一个特定的值,该值其实就是顾客购物折扣百分率。而在Customer类中,Payment属性就通过强类型转换来获取枚举成员的值(也就是购物折扣率),并用于货款计算。从这里可以看出,获取枚举成员的值还可以通过强类型转换方式。


Q:既然枚举类型可以强制转换为整数,那么整数是否也可以强制转换为枚举类型?

A:答案是肯定的。

  // Code #14
  
// See Code #01 for Alignment.
  Alignment a 

抱歉!评论已关闭.