关于枚举的种种 (Enumeration FAQ) [C#, IL, BCL]
关于枚举的种种 [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):
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代码如下:
.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的声明:
请注意,.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),但本身却是一个引用类型。
好吧,现在来看看下面代码,你能猜得出它的输出结果吗?
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#代码:
// See Code #01 for Alignment.
static void Main()
{
Alignment a = Alignment.Center;
Console.WriteLine(a.ToString());
Console.WriteLine(a);
}
对应的IL代码是:
.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:
// 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:
.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中的
该语句明显是整数赋值,这是否说明枚举类型实质上是整数类型?
A:这说明枚举类型与整数类型的确有一定的关系。事实上,每一个枚举类型都有与之相对应的整数类型,我们称该整数类型为底层类型(underlying type),默认的情况下使用,.NET使用System.Int32。当然,你可以手动将其指定为其他的整数类型:
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。例如:
public enum Alignment
{
Left, // 0
Center, // 1
Right // 2
}
Q:如果我有手动指定某些成员的值呢?
A:那么被赋值的成员的值就是你所指定的值。当然,无论你是否手动指定枚举成员的值,递增步长都不会变,总是为1。为了测试你是否理解,请说出下面枚举个成员的值以及你的判断理由(请用人脑而不是电脑来运行以下代码):
public enum DriveType : sbyte
{
CDRom,
Fixed = -2,
Network,
NoRootDirectory = -1,
Ram,
Removable = Network * NoRootDirectory,
Unknown
}
Q:我们如何获取枚举成员的值,无论成员是否被手动赋值?
A:你可以使用System.Enum的
该方法返回一个包含所有枚举成员的数组:
// 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接口,再调用对应的方法:
// 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:一般情况下,使用默认的赋值规则就足够了,但某些情况下,为枚举成员指定一个与实际情况(模型)相符的值可能更有意义,这要视你具体所建的模型而定。
还是让我们来一个实际的例子:
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:答案是肯定的。
// See Code #01 for Alignment.
Alignment a = (Alignment)1;
但这种机制可能使你遇到一些麻烦:
// See Code #01 for Alignment.
class Program
{
static void Main()
{
Foo((Alignment)12345);
}
static void Foo(Alignment a)
{
// Code here
}
}
你无法避免有人进行这样的恶作剧!!
Q:那么是否有办法对付这些恶作剧的人?
A:Sure!我们总不能假设人人都那么守规矩,所以,我们需要System.Enum的
现在我们把Code #15的Foo方法改进一下:
// See Code #01 for Alignment.
static void Foo(Alignment a)
{
if (!Enum.IsDefined(typeof(Alignment), a))
throw new ArgumentException("DO NOT MAKE MISCHIEF!");
// Code here
}
这样,恶作剧的人将会收到一个警告(异常消息)。当然,我们不排除有人是由于一时大意才造成这样的“恶作剧”,那么IsDefined方法同样可以帮助你处理好这些情况。
Q:我认为我们还可以使用条件判断语句来处理这种情况:
// See Code #01 for Alignment.
static void Foo(Alignment a)
{
if (a != Alignment.Left &&
a != Alignment.Center &&
a != Alignment.Right)
throw new ArgumentException("DO NOT MAKE MISCHIEF!");
// Code here
}
或者
// See Code #01 for Alignment.
static void Foo(Alignment a)
{
switch(a)
{
case Alignment.Left:
Console.WriteLine("Cool~");
break;
case Alignment.Center:
Console.WriteLine("Well~");
break;
case Alignment.Right:
Console.WriteLine("Good~");
break;
default:
Console.WriteLine("DO NOT MAKE MISCHIEF!");
break;
}
}
A:你绝对可以这样做!事实上,如果你处于以下情况之一的话:
- 1. Alignment枚举代码不会被修改
- 2. 你不希望使用Alignment枚举新增的特性
那么我会推荐使用你的处理方式。而且,你还可以为自己的代码定义一个这样的方法:
// See Code #01 for Alignment.
public static bool IsAlignment(Alignment a)
{
switch(a)
{
case Alignment.Left:
return true;
case Alignment.Center:
return true;
case Alignment.Right:
return true;
default:
return false;
}
}
这个方法比起IsDefine方法高效多了。
Q:我定义了一个这样的枚举:
public enum FontStyle
{
Bold,
Italic,
Regular,
Strikethrough,
Underline
}
我用它来指定字体的风格,但我遇到了麻烦。你知道,字体可以同时拥有枚举里面所列举的一种或者多种风格,那么,我如何为字体同时指定多种风格呢?
A:这个时候你就需要位枚举(Bit Flags),把Code #20修改一下:
// I am using the FlagsAttribute to identify a bit flags.
[Flags]
public enum FontStyle
{
Bold = 0x0001,
Italic = 0x0002,
Regular = 0x0004,
Strikethrough = 0x0010,
Underline = 0x0020
}
现在,你可以通过按位或运算来为字体指定多种风格了:
// See Code #21 for FontStyle.
Font f = new Font(
FontFamily.GenericSansSerif,
12.0F,
FontStyle.Italic | FontStyle.Underline
);
Q:位枚举同样存在类似于Code #15的恶作剧吧?
A:是的,例如:
// See Code #21 for FontStyle.
class Program
{
static void Main()
{
Bar(FontStyle.Regular | (FontStyle)0x0400);
}
static void Bar(FontStyle fs)
{
// Code here
}
}
Q:那么,System.Enum.IsDefine方法是否还能应对呢?
A:不能。位枚举成员并不具备排他性,多个成员可以通过按位或运算组合起来。而System.Enum.IsDefine方法只能判断枚举变量的值是否为某一已定义的枚举成员。请看如下代码:
// See Code #21 for FontStyle.
FontStyle fs1 = FontStyle.Bold | FontStyle.Italic | FontStyle.Underline;
Console.WriteLine(Enum.IsDefine(typeof(FontStyle), fs1));
FontStyle fs2 = FontStyle.Regular | (FontStyle)0x0400;
Console.WriteLine(Enum.IsDefine(typeof(FontStyle), fs2));
// Output:
// false
// false
我们对代码的输出毫无疑问,因为fs1和fs2都不是一个单独的枚举成员。但这不是我们所追求的答案,我们希望区别对待fs1和fs2,至少我们不希望fs2中的捣蛋家伙——(FontStyle)0x0400——在我们的程序中搞破坏!
Q:那么,我们是否有办法隔离这些捣蛋鬼呢?
A:Of course!我们同样可以使用条件判断语句来处理,但做法将与Code #17和Code #18有所不同。现在我们把Code #23改进如下:
// See Code #21 for FontStyle.
class Program
{
static void Main()
{
Bar(FontStyle.Regular | (FontStyle)0x0400);
}
static void Bar(FontStyle fs)
{
if ((fs & FontStyle.Bold) != 0)
{
// Do something associated with bold
}
if ((fs & FontStyle.Italic) != 0)
{
// Do something associated with italic
}
// Other conditional code continues here
}
}
我们把枚举变量与某一特定的位枚举成员进行按位与运算,若结果不为0则表明枚举变量中包含着该位枚举成员。当然,你也可以为自己的代码写一组这样的方法:
// See Code #21 for FontStyle.
static bool ContainsBold(FontStyle fs)
{
if ((fs & FontStyle.Bold) != 0)
return true;
else
return false;
}
static bool ContainsItalic(FontStyle fs)
{
if ((fs & FontStyle.Italic) != 0)
return true;
else
return false;
}
// Other similar methods continue here
又或者你可以写一个这样的方法:
// See Code #21 for FontStyle.
static bool ContainsMember(FontStyle fs, FontStyle menber)
{
if ((fs & member) != 0)
return true;
else
return false;
}
如果你只希望判断某一个枚举变量里面是否包含捣蛋鬼,你可以写一个这样的方法:
// See Code #21 for FontStyle.
static bool ContainsPranksters(FontStyle fs)
{
if ((fs < FontStyle.Bold) || (fs > (FontStyle)0x0037))
return true;
if (fs == (FontStyle)0x0008 ||
fs == (FontStyle)0x0009 ||
fs == (FontStyle)0x0018 ||
fs == (FontStyle)0x0019 ||
fs == (FontStyle)0x0028 ||
fs == (FontStyle)0x0029)
return true;
return false;
}
留个“作业”吧,知道为何这样可以判断出是否有捣蛋鬼吗?当然,如果你想到了更好的方法,记住要告诉我哟!
Q:慢着!你那个“我们把枚举变量与某一特定的位枚举成员进行按位与运算,若结果不为0则表明枚举变量中包含着该位枚举成员”,在以下的情况显然不成立的:
[Flags]
enum Music
{
Jazz = 0x00,
Rock = 0x01,
Country = 0x02,
Classic = 0x03
}
static void Main()
{
Music m = Music.Rock | Music.Jazz;
int r = (int)(m & Music.Classic);
Console.WriteLine(r);
}
该代码的输出恰恰就为0,然而m却不包含Music.Classic!
A:Good question!也正如你所看到的,这种做法其实与位枚举成员的值是如何被赋予息息相关的。那么,我们应该如何为位枚举的成员赋值呢?由于位枚举成员的值和位运算都直接与二进制相关,所以,我们不妨从二进制的角度去探索一下如何恰当的为位枚举的成员赋值。
试想一下,如果我们能把二进制值的字面特征与位枚举成员关联起来,使得我们能够直接从二进制值的字面特征判断位枚举成员的存在与否该多好呀!
考察你的Music枚举,它有4个成员,那么我们把二进制值的数位设定为4,即[D][C][B][A]型;并规定每一个数位代表该一个枚举成员,即A代表Music.Jazz、B代表Music.Rock、C代表Music.Country、D代表Music.Classic;那么,某个数位的值为1就代表其对应的枚举成员存在,为0则不存在。现在,假如Music的某个变量m的二进制值为0110,我们就可以肯定的说,m中包含着Music.Rock和Music.Country,因其B、C数位的值均为1。
那么这些跟为位枚举成员赋值有什么关系呢?从上面的讨论可以知道,Music各个成员的二进制值分别为:Music.Jazz为0001、Music.Rock为0010、Music.Country为0100、Music.Classic为1000。把这些值转换为十六进制值并赋予对应的成员:
[Flags]
enum Music
{
Jazz = 0x01,
Rock = 0x02,
Country = 0x04,
Classic = 0x08
}
这样,你就可以采用我所提到的方法来验证某个枚举变量中是否包含着特定的枚举成员了。
Q:如何把枚举类型转换(解析)成字符串类型?
A:最简单的方法就是使用System.Enum的
方法,或者把枚举类型转换为IConvertible接口,再调用该接口的
方法。此时你将得到枚举成员的字面值的字符串:
// See Code #01 for Alignment.
// See Code #21 for FontStyle.
static void Main()
{
Alignment a = Alignment.Right;
Console.WriteLine("Alignment is {0}.", a.ToString());
FontStyle fs = FontStyle.Bold | FontStyle.Underline;
Console.WriteLine("FontStyle is {0}.", fs.ToString());
}
// Output:
// Alignment is Right.
// FontStyle is Bold, Underline.
如果你希望输出枚举成员的值,那么你可以手动指定格式参数:
// See Code #01 for Alignment.
// See Code #21 for FontStyle.
static void Main()
{
Alignment a = Alignment.Right;
// Represents Alignment in decimal form.
Console.WriteLine("Alignment is {0}.", a.ToString("d"));
// Represents Alignment in hexadecimal without a leading "0x".
Console.WriteLine("Alignment is {0}.", a.ToString("x"));
FontStyle fs = FontStyle.Bold | FontStyle.Underline;
// Represents FontStyle in decimal form.
Console.WriteLine("FontStyle is {0}.", fs.ToString("d"));
// Represents FontStyle in hexadecimal without a leading "0x".
Console.WriteLine("FontStyle is {0}.", fs.ToString("x"));
}
// Output:
// Alignment is 2.
// Alignment is 00000002.
// FontStyle is 33.
// FontStyle is 00000021.
除此之外,你还可以使用System.Enum的
Type enumType,
object value,
string format
);
方法:
// See Code #01 for Alignment.
static void Main()
{
Alignment a = Alignment.Right;
Console.WriteLine(
"Alignment is 0x{0}.",
System.Enum.Format(typeof(Alignment), a, "x")
);
Console.WriteLine("Alignment is 0x{0:x}.", a);
}
// Output:
// Alignment is 0x00000002.
// Alignment is 0x00000002.
另外,你还可以通过System.Enum的
来获取枚举所有成员的字面值:
// See Code #01 for Alignment.
static void Main()
{
string[] names = Enum.GetNames(typeof(Alignment));
foreach(string name in names)
Console.WriteLine(name);
}
// Output:
// Left
// Center
// Right
Q:如果我得到一个表示枚举成员的字符串,我如何将其解析为对应枚举类型呢?
A:这时你就需要System.Enum的
Type enumType,
string value,
bool ignoreCase
);
方法了:
// See Code #01 for Alignment.
// See Code #21 for FontStyle.
static void Main()
{
string name = "Right";
Alignment a = (Alignment)Enum.Parse(typeof(Alignment), name, false);
Console.WriteLine(a.ToString());
string names = "Bold, Italic, Underline";
FontStyle fs = (FontStyle)Enum.Parse(typeof(FontStyle), names, false);
Console.WriteLine(fs.ToString());
}
// Output:
// Right
// Bold, Italic, Underline
Q:枚举类型为我们编码提供了巨大的便利,什么情况下我们不应该使用枚举呢?
A:首先你应该清楚枚举类型在编程中充当一个什么样的角色。在我看来,枚举类型表达了一种稳定的分类标准。当你查看.NET Framework BCL中的枚举类型,你会发现它们几乎没有任何改变的可能或者趋势,表现出一种稳定性。所以,当你所要表达的分类标准也同样具备这种稳定性时,你就可以考虑枚举类型了。那么什么情况下不使用枚举呢?一般说来,当分类标准不闭合时——即新的子分类随时有可能产生或者现有子分类随时有可能被替换——你就应该考虑使用其他的方式来表达了。
下面让我们来看一个薪酬自动管理系统的一部分,假设某公司现有雇员种类为:
- 1. Programmer
- 2. Salesman
- 3. Manager
相关代码如下:
public enum EmployeeKind
{
Programmer,
Salesman,
Manager
}
public class Employee
{
public Employee(string name, EmployeeKind kind)
{
Kind = kind;
}
private string m_Name;
public string Name
{
get { return m_Name; }
}
public readonly EmployeeKind Kind;
public double GetPayment()
{
switch(Kind)
{
case EmployeeKind.Programmer:
// Return payment
case EmployeeKind.Salesman:
// Return payment
case EmployeeKind.Manager:
// Return payment
}
}
}
假如该公司正处于成长期,那么公司的组织结构发生改变是家常便饭之事。但公司每一次改变组织结构,这样的系统就要经历源代码修改、重新编译然后再部署的过程!而且,如果公司实行了新的绩效评估方案,并把薪酬计算与绩效追踪挂钩,那么GetPayment方法将进一步壮大,以至于最终其代码晦涩难懂。你当然可以把GetPayment分割为子方法,但并没有什么实质的改变。再想一想,如果将来公司打算把薪酬系统与银行挂钩,提供薪资直接银行划拨,那将又会是怎么一番局面呢?
好吧,我承认我不太愿意接手维护这样的系统,但讨论如何改进这个系统会使本文进一步壮大,以至于最终其内容晦涩难懂,不过你应该可以从我的《今天你多态了吗?》中找到一些改进灵感。