一、枚举类型
枚举类型(enumerated types)定义了一组"符号名称/值"配对。
例如,以下Color类型定义了一组符号,每个符号都标识一种颜色:
internal enum Color { While, //赋值0 Red, //赋值1 Green, //赋值2 Blue, //赋值3 Orange //赋值4 }
使用枚举类型的好处:
1)枚举类型使程序更容易编写、阅读和维护。有了枚举类型,符号名称可在代码中随便使用,开发人员不需要记 住每个硬编码的含义。而且,一旦与符号名称对应的值发生变化,代码也可以简单的重新编译,不需要对源代码做出任何修改。除此之外,文档工具和其他实用程序能向开发人员显示有意义的符号名称。
2)枚举类型是强类型的。
在Microsoft .NET Framework中,枚举类型不只是编译器所关心的符号,它在类型系统中还具有"一等公民"的地 位,能实现非常强大的操作。枚举类型都直接从System.Enum派生,后者从System.ValueType派生,而System.ValueType用从 System.Object派生。所以,枚举类型是值类型,可表示成未装箱和已装箱形式。然而,有别于其他值类型,枚 举类型不能定义任何方法、属性和事件。不过可利用C#的"扩展方法"功能模拟向枚举类型添加方法。
编译枚举类型时,C#编译器会把每个符号转换成为类型的一个常量字段。例如,编译器会把前面的Color枚举类型看成以下代码:
internal struct Color : System.Enum { //以下是一些公共常量,它们定义了Color的符号和值 public const Color While = (Color)0; public const Color Red = (Color)1; public const Color Green = (Color)2; public const Color Bule = (Color)3; public const Color Orange = (Color)4; //以下是一个公共实例字段,它包含一个Color变量的值, //不能写代码来直接引用这个实例字段 public Int32 value__; }
C#编译器实际上并不编译这段代码,因为它禁止定义从System.Enum这一特殊类型派生的类型。不过,可以通过 上述伪类型定义了解内部工作方式。简单的说,枚举类型只是一个结构,其中定义了一组常量字段和一个实例字 段。常量字段会嵌入程序集的元数据中,并可以通过反射来访问。这意味着可以在运行时获得与枚举类型关联的 所有符号及其值。还意味着可以将一个字符串符号转换成对应的数值。这些操作是通过System.Enum基类型来提 供的,该类型提供了几个静态和实例方法,可利用它们操作枚举类型的一个实例,从而避免了必须使用反射的麻
烦。
提示:枚举类型定义的符号是常量值。所以当编译器发现代码引用了一个枚举类型的符号,就会在编译时用数值 替换符号,代码将不再引用定义了符号的枚举类型。这意味着在运行时可能不需要定义了枚举类型的程序集,在
编译时需要。
例如,System.Enum类型有一个GetUnderlyingType的静态方法,而System.Type类型有一个 GetEnumUnderlyingType的实例方法。
public static Type GetUnderlyingType (Type enumType); //System.Enum中定义 public Type GetEnumUnderlyingType (Type enumType); //System.Type中定义
这些方法返回用于容纳一个枚举类型的值的基础类型。每个枚举类型都有一个基础类型,它可以是byte,sbyte, short,ushort,int(最常用,也是C#默认的),uint,long,或ulong。虽然这些C#基元类型都有都有对象的FCL 类型,但C#编译器为了简化本身的实现,要求只能指定基元类型名称。如果使用FCL类型名称(如Int32),就会报
错。
以下代码演示了如何声明一个基础类型为byte(System.Byte)的枚举类型:
inter enum Color :byte { While, Red, Green, Blue, Orange }
基于这个Color枚举类型,一下代码显示了GetUnderlyingType 的返回结果:
//以下代码会显示"System.Byte" Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));
C#编译器将枚举类型视为基元类型。所以,可以运用许多操作符(==,!=,<,>等等)来操作枚举类型的实例。 所有这些操作符实际作用于枚举类型实例内部的value__实例字段。此外,C#编译器还运行将枚举类型的实例显式 的转型为一个不通过的枚举类型。也可以显式将一个枚举类型实例转型为一个数值类型。
可以调用System.Enum的静态方法GetValue或者System.Type的实例方法GetEnumValue获取一个数组,该数组的每一个元素都对应枚举类型中的一个符号名称,每个元素都包含符号名称的数值:
public static Array GetValues(Type enumType); //System.Enum中定义 public Array GetEnumValues(Type enumType); //System.Type中定义
这个方法结合ToString方法使用,可显示枚举类型中所有符号名称及其对应的数值,如下所示:
public static void Go() { Color[] colors = (Color[])Enum.GetValues(typeof(Color)); Console.WriteLine("Number of symbols defined: " + colors.Length); Console.WriteLine("Value\tSymbol\n-----\t------"); foreach (Color color in colors) { // 以十进制和常规格式显示每个符号 Console.WriteLine("{0,5:D}\t{0:G}", color); } }
以上代码产生的输出如下:
Number of symbols defined: 5
Value Symbol
----- ------
0 While
1 Red
2 Green
3 Blue
4 Orange
还有其他枚举类型成员,就不一一叙述了!
二、位标志
程序员经常要与位标识(bit flag)集合打交道。调用System.IO.File类型的GetAttributes方法,会返回FileAttributes类型的一个实例。FileAttributes类型是基本类型为Int32的枚举类型,其中每一位都反映了文件的一项属性。FileAttibutes类型在FCL中的定义如下:
[Flags] [Serializable] [ComVisible (true)] public enum FileAttributes { Archive = 0x00020, Compressed = 0x00800, Device = 0x00040, // Reserved for future use (NOT the w32 value). Directory = 0x00010, Encrypted = 0x04000, // NOT the w32 value Hidden = 0x00002, Normal = 0x00080, NotContentIndexed = 0x02000, Offline = 0x01000, ReadOnly = 0x00001, ReparsePoint = 0x00400, SparseFile = 0x00200, System = 0x00004, Temporary = 0x00100, #if NET_4_5 IntegrityStream = 0x8000, NoScrubData = 0x20000, #endif }
为了判断一个文件是否隐藏,可执行下面这样的代码:
String file = Assembly.GetEntryAssembly().Location; FileAttributes attributes = File.GetAttributes(file); Console.WriteLine("Is {0} hidden? {1}",file,(attributes & FileAttributes.Hidden) !=0);
以下代码演示了如何将一个文件的属性改为只读和隐藏:
File.SetAttributes(file,FileAttributes.ReadOnly | FileAttribute.Hidden);
正如FileAttributes类型展示的那样,经常都要用枚举类型来表示一组可以组合的位标志。不过,虽然枚举类型和位标志相似,但它们的语义不尽相同。例如,枚举类型表示单个数值,而位标识表示一组位,其中有些位是1,有 些位是0.
定义用于标识位标志的枚举类型时,当然应该显式为每个符号分配一个数值。通常,每个符号都有单独的一个位处于on(1)状态.此外,经常都要定义一个值为0的None符号。还可以定义一些代表常用位组合的符号。另外,强烈建议向枚举类型应用System.Flags.Attribute这个定制的attribute类型,如下所示
[Flags] public enum Actions { Read = 0x0001, Write = 0x0002, ReadWrite = Actions.Read | Actions.Write, Delete = 0x0004, Query = 0x0008, Sync = 0x0010 }
因为Actions是枚举类型,所以在操作位标志枚举类型时,可以使用上一节描述的所有方法。
Actions actions = Actions.Read | Actions.Delete; //0x0005 Console.WriteLine(actions.ToString()); //"Read,Delete"
调用ToString时,它会视图将数值转换为对应的符号。现在的数值是0x0005,它没有对应的符号。不过,ToString方法检测到Actions类型上存在[Flags]这个attribute,所以ToString方法现在不会将该数值视为单独的值。相反,会将它视为一组位标志。由于0x0005有0x0001和0x0004组合而成,所以ToString会生成字符串"Read,Delete", 如果从Actions类型中删除[Flags]这个attribute,ToString方法返回"5"。
永远不要对位标志枚举类型使用IsDefined方法,理由如下:
1)如果向IsDefined方法传递一个字符串,它不会将这个字符串拆分为单独的token来进行查找,而是视图查找整个字符串,把它看成是包含逗号的一个更大的符号。由于不能在枚举类型中定义含有逗号的符号,所以这个符号永远找不到。
2)如果向IsDefined方法传递一个数值,它会检查枚举类型是否定义了一个其对应数值和传入数值匹配的符号。由于位标志不能这样简单匹配,所以IsDefined通常会返回flase。
三、向枚举类型添加方法
现在,可以使用C#的扩展方法功能向枚举类型模拟添加方法。
如果想为FileAttributes枚举类型添加一些方法,可以定义一个包含了扩展方法的静态类,如下所示:
public static Boolean Set(this FileAttributes flags, FileAttributes testFlags) { return flags | testFlags; }
从表面看,我似乎真的在枚举类型上调用这些方法:
FileAttributes fa = FileAttributes.System;
fa = fa.Set(FileAttributes.ReadOnly);