8.4 简易不简单:认识枚举
本节将介绍以下内容:
— 枚举类型全解
— 位标记应用
— 枚举应用规则
8.4.1 引言
在哪里可以看到枚举?打开每个文件的属性,我们会看到只读、隐藏的选项;操作一个文件时,你可以采用只读、可写、追加等模式;设置系统级别时,你可能会选择紧急、普通和不紧急来定义。这些各式各样的信息中,一个共同的特点是信息的状态分类相对稳定,在.NET中可以选择以类的静态字段来表达这种简单的分类结构,但是更明智的选择显然是:枚举。
事实上,在.NET中有大量的枚举来表达这种简单而稳定的结构,FCL中对文件属性的定义为System.IO.FileAttributes枚举,对字体风格的定义为System.Drawing.FontStyle枚举,对文化类型定义为System.Globlization.CultureType枚举。除了良好的可读性、易于维护、强类型的优点之外,性能的考虑也占了一席之地。
关于枚举,在本节会给出详细而全面的理解,认识枚举,从一点一滴开始。
8.4.2 枚举类型解析
1.类型本质
所有枚举类型都隐式而且只能隐式地继承自System.Enum类型,System.Enum类型是继承自System.ValueType类型唯一不为值类型的引用类型。该类型的定义为:
public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible
从该定义中,我们可以得出以下结论:
l System.Enum类型是引用类型,并且是一个抽象类。
l System.Enum类型继承自System.ValueType类型,而ValueType类型是一切值类型的根类,但是显然System.Enum并非值类型,这是ValueType唯一的特例。
l System.Enum类型实现了IComparable、IFormattable和IConvertible接口,因此枚举类型可以与这三个接口实现类型转换。
.NET之所以在ValueType之下实现一个Enum类型,主要是实现对枚举类型公共成员与公共方法的抽象,任何枚举类型都自动继承了Enum中实现的方法。关于枚举类型与Enum类型的关系,可以表述为:枚举类型是值类型,分配于线程的堆栈上,自动继承于Enum类型,但是本身不能被继承;Enum类型是引用类型,分配于托管堆上,Enum类型本身不是枚举类型,但是提供了操作枚举类型的共用方法。
下面我们根据一个枚举的定义和操作来分析其IL,以从中获取关于枚举的更多认识:
enum LogLevel
{
Trace,
Debug,
Information,
Warnning,
Error,
Fatal
}
将上述枚举定义用Reflector工具翻译为IL代码,对应为:
.class private auto ansi sealed LogLevel
extends [mscorlib]System.Enum
{
.field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Debug = int32(1)
.field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Error = int32(4)
.field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Fatal = int32(5)
.field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Information = int32(2)
.field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Trace = int32(0)
.field public specialname rtspecialname int32 value__
.field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Warnning = int32(3)
}
从上述IL代码中,LogLevel枚举类型的确继承自System.Enum类型,并且编译器自动为各个成员映射一个常数值,默认从0开始,逐个加1。因此,在本质上枚举就是一个常数集合,各个成员常量相当于类的静态字段。
然后,我们对该枚举类型进行简单的操作,以了解其运行时信息,例如:
public static void Main()
{
LogLevel logger = LogLevel.Information;
Console.WriteLine("The log level is {0}.", logger);
}
该过程实例化了一个枚举变量,并将它输出到控制台,对应的IL为:
.method public hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] valuetype InsideDotNet.Framework.EnumEx.LogLevel logger)
L_0000: nop
L_0001: ldc.i4.2
L_0002: stloc.0
L_0003: ldstr "The log level is {0}."
L_0008: ldloc.0
L_0009: box InsideDotNet.Framework.EnumEx.LogLevel
L_000e: call void [mscorlib]System.Console::WriteLine(string, object)
L_0013: nop
L_0014: ret
}
分析IL可知,首先将2赋值给logger,然后执行装箱操作(L_0009),再调用WriteLine方法将结果输出到控制台。
2.枚举规则
讨论了枚举的本质,我们再回过头来,看看枚举类型的定义及其规则,例如下面的枚举定义略有不同:
enum Week: int
{
Sun = 7,
Mon = 1,
Tue,
Wed,
Thur,
Fri,
Sat,
Weekend = Sun
}
根据以上定义,我们了解关于枚举的种种规则,这些规则是定义枚举和操作枚举的基本纲领,主要包括:
l 枚举定义时可以声明其基础类型,例如本例Week枚举的基础类型指明为int型,默认情况时即为int。通过指定类型限定了枚举成员的取值范围,而被指定为枚举声明类型的只能是除char外的8种整数类型:byte、sbyte、short、ushort、int、uint、long和ulong,声明其他的类型将导致编译错误,例如Int16、Int64。
l 枚举成员是枚举类型的命名常量,任意两个枚举常量不能具有同样的名称符号,但是可以具有相同的关联值。
l 枚举成员会显式或者隐式与整数值相关联,默认情况下,第一个元素对应的隐式值为0,然后各个成员依次递增1。还可以通过显式强制指定,例如Sun为7,Mon为1,而Tue则为2,并且成员Weekend和Sun则关联了相同的枚举值。
l 枚举成员可以自由引用其他成员的设定值,但是一定注意避免循环定义,否则将引发编译错误,例如:
enum MusicType
{
Blue,
Jazz = Pop,
Pop
}
编译器将无法确知成员Jazz和Pop的设定值到底为多少。
l 枚举是一种特殊的值类型,不能定义任何的属性、方法和事件,枚举类型的属性、方法和事件都继承自System.Enum类型。
l 枚举类型是值类型,可以直接通过赋值进行实例化,例如:
Week myweek = Week.Mon;
也可以以new关键字来实例化,例如:
Week myweek = new Week();
值得注意的是,此时myweek并不等于Week枚举类型中定义的第一个成员的Sun的关联值7,而是等效于字面值为0的成员项。如果枚举成员不存在0值常数,则myweek将默认设定为0,可以从下面代码来验证这一规则:
enum WithZero
{
First = 1,
Zero = 0
}
enum WithNonZero
{
First = 1,
Second
}
class EnumMethod
{
public static void Main()
{
WithZero wz = new WithZero();
Console.WriteLine(wz.ToString("G"));
WithNonZero wnz = new WithNonZero();
Console.WriteLine(wnz.ToString("G"));
}
}
//执行结果
//Zero
//0
因此,以new关键字来实例化枚举类型,并非好的选择,通常情况下我们应该避免这种操作方式。
l 枚举可以进行自增自减操作,例如:
Week day = (Week)3;
day++;
Console.WriteLine(day.ToString());
通过自增运算,上述代码输出结果将为:Fri。
8.4.3 枚举种种
1.类型转换
(1)与整型转换
因为枚举类型本质上是整数类型的集合,因此可以与整数类型进行相互的类型转换,但是这种转换必须是显式的。
//枚举转换为整数
int i = (int)Week.Sun;
//将整数转换为枚举
Week day = (Week)3;
另外,Enum还实现了Parse方法来间接完成整数类型向枚举类型的转换,例如:
//或使用Parse方法进行转换
Week day = (Week)Enum.Parse(typeof(Week), "2");
(2)与字符串的映射
枚举与String类型的转换,其实是枚举成员与字符串表达式的相互映射,这种映射主要通过Enum类型的两个方法来完成:
l ToString实例方法,将枚举类型映射为字符串表达形式。可以通过指定格式化标志来输出枚举成员的特定格式,例如“G”表示返回普通格式、“X”表示返回16进制格式,而本例中的“D”则表示返回十进制格式。
l Parse静态方法,将整数或者符号名称字符串转换为等效的枚举类型,转换不成功则抛出ArgumentException异常,例如:
Week myday = (Week)Enum.Parse(typeof(Week), "Mon", true);
Console.WriteLine(myday);
因此,Parse之前最好应用IsDefined方法进行有效性判断。对于关联相同整数值的枚举成员,Parse方法将返回第一个关联的枚举类型,例如:
Week theDay = (Week)Enum.Parse(typeof(Week), "7");
Console.WriteLine(theDay.ToString());
//执行结果
//Sun
(3)不同枚举的相互转换
不同的枚举类型之间可以进行相互转换,这种转换的基础是枚举成员本质为整数类型的集合,因此其过程相当于将一种枚举转换为值,然后再将该值映射到另一枚举的成员。
MusicType mtToday = MusicType.Jazz;
Week today = (Week)mtToday;
(4)与其它引用类型转换
除了可以显式的与8种整数类型进行转换之外,枚举类型是典型的值类型,可以向上转换为父级类和实现的接口类型,而这种转换实质发生了装箱操作。小结枚举可装箱的类型主要包括:System.Object、System.ValueType、System.Enum、System.IComparable、System.IFormattable和System.IConvertible。例如:
IConvertible iConvert = (IConvertible)MusicType.Jazz;
Int32 x = iConvert.ToInt32(CultureInfo.CurrentCulture);
Console.WriteLine(x);
1.常用方法
System.Enum类型为枚举类型提供了几个值得研究的方法,这些方法是操作和使用枚举的利器,由于System.Enum是抽象类,Enum方法大都是静态方法,在此仅举几个简单的例子点到为止。
以GetNames和GetValues方法分别获取枚举中符号名称数组和所有符号的数组,例如:
//由GetName获取枚举常数名称的数组
foreach (string item in Enum.GetNames(typeof(Week)))
{
Console.WriteLine(item.ToString());
}
//由GetValues获取枚举常数值的数组
foreach (Week item in Enum.GetValues(typeof(Week)))
{
Console.WriteLine("{0} : {1}", item.ToString("D"), item.ToString());
}
应用GetValues方法或GetNames方法,可以很容易将枚举类型与数据显式控件绑定来显式枚举成员,例如:
ListBox lb = new ListBox();
lb.DataSource = Enum.GetValues(typeof(Week));
this.Controls.Add(lb);
以IsDefined方法来判断符号或者整数存在于枚举中,以防止在类型转换时的越界情况出现。
if(Enum.IsDefined(typeof(Week), "Fri"))
{
Console.WriteLine("Today is {0}.", Week.Fri.ToString("G"));
}
以GetUnderlyingType静态方法,返回枚举实例的声明类型,例如:
Console.WriteLine(Enum.GetUnderlyingType(typeof(Week)));
8.4.4 位枚举
位标记集合是一种由组合出现的元素形成的列表,通常设计为以“位或”运算组合新值;枚举类型则通常表达一种语义相对独立的数值集合。而以枚举类型来实现位标记集合是最为完美的组合,简称为位枚举。在.NET中,需要对枚举常量进行位运算时,通常以System.FlagsAttribute特性来标记枚举类型,例如:
[Flags]
enum ColorStyle
{
None = 0x00,
Red = 0x01,
Orange = 0x02,
Yellow = 0x04,
Greeen = 0x08,
Blue = 0x10,
Indigotic = 0x20,
Purple = 0x40,
All = Red | Orange | Yellow | Greeen | Blue | Indigotic | Purple
}
FlagsAttribute特性的作用是将枚举成员处理为位标记,而不是孤立的常数,例如:
public static void Main()
{
ColorStyle mycs = ColorStyle.Red | ColorStyle.Yellow | ColorStyle.Blue;
Console.WriteLine(mycs.ToString());
}
在上例中,mycs实例的对应数值为21(十六进制0x15),而覆写的ToString方法在ColorStyle枚举中找不到对应的符号。而FlagsAttribute特性的作用是将枚举常数看成一组位标记来操作,从而影响ToString、Parse和Format方法的执行行为。在ColorStyle定义中0x15显然由0x01、0x04和0x10组合而成,示例的结果将返回:Red, Yellow, Blue,而非21,原因正在于此。
位枚举首先是一个枚举类型,因此具有一般枚举类型应有的所有特性和方法,例如继承于Enum类型,实现了ToString、Parse、GetValues等方法。但是由于位枚举的特殊性质,因此应用于某些方法时,应该留意其处理方式的不同之处。这些区别主要包括:
l Enum.IsDefined方法不能应对位枚举成员,正如前文所言位枚举区别与普通枚举的重要表现是:位枚举不具备排他性,成员之间可以通过位运算进行组合。而IsDefined方法只能应对已定义的成员判断,而无法处理组合而成的位枚举,因此结果将总是返回false。例如:
Enum.IsDefined(typeof(ColorStyle), 0x15)
Enum.IsDefined(typeof(ColorStyle), "Red, Yellow, Blue")
MSDN中给出了解决位枚举成员是否定义的判断方法:就是将该数值与枚举成员进行“位与”运算,结果不为0则表示该变量中包含该枚举成员,例如:
if ((mycs & ColorStyle.Red) != 0)
Console.WriteLine(ColorStyle.Red + " is in ColorStyle");
l Flags特性影响ToString、Parse和Format方法的执行过程和结果。
l 如果不使用FlagsAttribute特性来标记位枚举,也可以在ToString方法中传入“F”格式来获得同样的结果,以“D”、“G”等标记来格式化处理,也能获得相应的输出格式。
l 在位枚举中,应该显式的为每个枚举成员赋予有效的数值,并且以2的幂次方为单位定义枚举常量,这样能保证实现枚举常量的各个标志不会重叠。当然你也可以指定其它的整数值,但是应该注意指定0值作为成员常数值时,“位与”运算将总是返回false。
8.4.5 规则与意义
l 枚举类型使代码更具可读性,理解清晰,易于维护。在Visual Stuido 2008等编译工具中,良好的智能感知为我们进行程序设计提供了更方便的代码机制。同时,如果枚举符号和对应的整数值发生变化,只需修改枚举定义即可,而不必在漫长的代码中进行修改。
l 枚举类型是强类型的,从而保证了系统安全性。而以类的静态字段实现的类似替代模型,不具有枚举的简单性和类型安全性。例如:
public static void Main()
{
LogLevel log = LogLevel.Information;
GetCurrentLog(log);
}
private static void GetCurrentLog(LogLevel level)
{
Console.WriteLine(level.ToString());
}
试图为GetCurrentLog方法传递整数或者其他类型参数将导致编译错误,枚举类型保证了类型的安全性。
l 枚举类型的默认值为0,因此,通常给枚举成员包含0值是有意义的,以避免0值游离于预定义集合,导致枚举变量保持非预定义值是没有意义的。另外,位枚举中与0值成员进行“位与”运算将永远返回false,因此不能将0值枚举成员作为“位与”运算的测试标志。
l 枚举的声明类型,必须是基于编译器的基元类型,而不能是对应的FCL类型,否则将导致编译错误。
8.4.6 结论
枚举类型在BCL中占有一席之地,说明了.NET框架对枚举类型的应用是广泛的。本节力图从枚举的各个方面建立对枚举的全面认知,通过枚举定义、枚举方法和枚举应用几个角度来阐释一个看似简单的概念,对枚举的理解与探索更进了一步。