认识枚举

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框架对枚举类型的应用是广泛的。本节力图从枚举的各个方面建立对枚举的全面认知,通过枚举定义、枚举方法和枚举应用几个角度来阐释一个看似简单的概念,对枚举的理解与探索更进了一步。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值