CLR via C#, 4th -- 【基本类型】 -- 第15章枚举类型和位标志
15.1 枚举类型
枚举类型(enumerated type)定义了一组“符号名称/值”配对。
internal enum Color { White, // Assigned a value of 0 Red, // Assigned a value of 1 Green, // Assigned a value of 2 Blue, // Assigned a value of 3 Orange // Assigned a value of 4 }
不应将这些数字硬码到代码中,而应使用枚举类型,理由至少有二。
- 枚举类型使程序更容易编写、阅读和维护。
- 枚举类型是强类型的。
每个枚举类型都直接从System.Enum派生,后者从System.ValueType派生,而System.ValueType又从System.Object派生。所以,枚举类型是值类型。但有别于其他值类型,枚举类型不能定义任何方法、属性或事件。不过,可利用C#的“扩展方法”功能模拟向枚举类型添加方法.
枚举类型只是一个结构
简单地说,枚举类型只是一个结构,其中定义了一组常量字段和一个实例字段。常量字段会嵌入程序集的元数据中,并可通过反射来访问。这意味着可以在运行时获得与枚举类型关联的所有符号及其值。还意味着可以将字符串符号转换成对应的数值。
枚举类型定义的符号是常量值。所以当编译器发现代码引用了枚举类型的符号时,会在编译时用数值替换符号,代码不再引用定义了符号的枚举类型。这意味着运行时可能并不需要定义了枚举类型的程序集,编译时才需要。
public static Type GetUnderlyingType(Type enumType); // Defined in System.Enum public Type GetEnumUnderlyingType(); // Defined in System.Type
每个枚举类型都有一个基础类型,它可以是byte,sbyte,short,ushort,int(最常用,也是C#默认选择的),uint,long或ulong。虽然这些C#基元类型都有对应的FCL类型,但C#编译器为了简化本身的实现,要求只能指定基元类型名称。
internal enum Color : byte { White, Red, Green, Blue, Orange }
// The following line displays "System.Byte". Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));
C#编译器将枚举类型视为基元类型。所以可用许多熟悉的操作符(-,!=,<,>,<=,>=, +,-,^,&,|,~,++和--)来操纵枚举类型的实例。所有这些操作符实际作用于每个枚举类型实例内部的value_实例字段。此外,C#编译器允许将枚举类型的实例显式转型为不同的枚举类型。也可显式将枚举类型实例转型为数值类型。
枚举映射为字符串
Color c = Color.Blue; Console.WriteLine(c); // "Blue" (General format) Console.WriteLine(c.ToString()); // "Blue" (General format) Console.WriteLine(c.ToString("G")); // "Blue" (General format) Console.WriteLine(c.ToString("D")); // "3" (Decimal format) Console.WriteLine(c.ToString("X")); // "03" (Hex format)
除了ToString方法,System.Enum类型还提供了静态Format方法,可调用它格式化枚举类型的值。
public static String Format(Type enumType, Object value, String format);
个人倾向于调用ToString方法,因为它需要的代码更少,而且更容易调用。但Format有一个ToString没有的优势:允许为value参数传递数值。这样就不一定要有枚举类型的实例。
// The following line displays "Blue". Console.WriteLine(Enum.Format(typeof(Color), (Byte)3, "G"));
也可调用System.Enum的静态方法GetValues或者System.Type的实例方法GetEnumValues来返回一个数组,数组中的每个元素都对应枚举类型中的一个符号名称,每个元素都包含符号名称的数值。
public static Array GetValues(Type enumType); // Defined in System.Enum public Array GetEnumValues(); // Defined in System.Type
Color[] colors = (Color[]) Enum.GetValues(typeof(Color)); Console.WriteLine("Number of symbols defined: " + colors.Length); Console.WriteLine("Value\tSymbol\n\t "); foreach (Color c in colors) { // Display each symbol in Decimal and General format. Console.WriteLine("{0,5:D}\t{0:G}", c); }
个人不喜欢GetValues和GetEnumValues方法,因为两者均返回一个Array,必须转型成恰当的数组类型。所以我总是定义自己的方法:
public static TEnum[] GetEnumValues<TEnum>() where TEnum : struct { return (TEnum[])Enum.GetValues(typeof(TEnum)); } Color[] colors = GetEnumValues<Color>();
在程序的UI元素(列表框、组合框等)中显示符号名称时,我认为经常使用的会是ToString方法(常规格式),前提是字符串不需要本地化(因为枚举类型没有提供本地化支持)。除了GetValues方法,System.Enum和System.Type类型还提供了其它方法来返回枚举类型的符号:
// Returns a String representation for the numeric value public static String GetName(Type enumType, Object value); // Defined in System.Enum public String GetEnumName(Object value); // Defined in System.Type // Returns an array of Strings: one per symbol defined in the enum public static String[] GetNames(Type enumType); // Defined in System.Enum public String[] GetEnumNames(); // Defined in System.Type
可利用这个操作转换用户在文本框中输入的一个符号。利用Enum提供的静态Parse和TryParse方法,可以很容易地将符号转换为枚举类型的实例
public static Object Parse(Type enumType, String value); public static Object Parse(Type enumType, String value, Boolean ignoreCase); public static Boolean TryParse<TEnum>(String value, out TEnum result) where TEnum: struct; public static Boolean TryParse<TEnum>(String value, Boolean ignoreCase, out TEnum result) where TEnum : struct;
// Because Orange is defined as 4, 'c' is initialized to 4. Color c = (Color) Enum.Parse(typeof(Color), "orange", true); // Because Brown isn't defined, an ArgumentException is thrown. c = (Color) Enum.Parse(typeof(Color), "Brown", false); // Creates an instance of the Color enum with a value of 1 Enum.TryParse<Color>("1", false, out c); // Creates an instance of the Color enum with a value of 23 Enum.TryParse<Color>("23", false, out c);
以下是Enum的静态IsDefined方法和Type的IsEnumDefined方法:
public static Boolean IsDefined(Type enumType, Object value); // Defined in System.Enum public Boolean IsEnumDefined(Object value); // Defined in System.Type
可利用IsDefined方法判断数值对于某枚举类型是否合法:
// Displays "True" because Color defines Red as 1 Console.WriteLine(Enum.IsDefined(typeof(Color), (Byte)1)); // Displays "True" because Color defines White as 0 Console.WriteLine(Enum.IsDefined(typeof(Color), "White")); // Displays "False" because a casesensitive check is performed Console.WriteLine(Enum.IsDefined(typeof(Color), "white")); // Displays "False" because Color doesn't have a symbol of value 10 Console.WriteLine(Enum.IsDefined(typeof(Color), (Byte)10));
IsDefined方法被经常用于参数校验,如下例所示:
public void SetColor(Color c) { if (!Enum.IsDefined(typeof(Color), c)) { throw(new ArgumentOutOfRangeException("c", c, "Invalid Color value.")); } // Set color to White, Red, Green, Blue, or Orange ... }
IsDefined方法很方便,但必须慎用。首先,IsDefined总是执行区分大小写的查找,而且完全没有办法让它执行不区分大小写的查找。其次,IsDefined相当慢,因为它在内部使用了反射。如果写代码来手动检查每一个可能的值,应用程序的性能极有可能变得更好。最后,只有当枚举类型本身在调用IsDefined的同一个程序集中定义时,才可使用IsDefined。
枚举类型总是要与另外某个类型结合使用,一般作为类型的方法参数或返回类型、属性和字段使用。枚举类型是嵌套定义在需要它的类型中,还是和该类型同级?检查FCL,会发现枚举类型通常与需要它的类同级。原因很简单,就是减少代码的录入量,使开发人员的工作变得更轻松。所以,除非担心名称冲突,否则你定义的枚举类型应该和需要它的类型同级。
15.2 位标志
[Flags, Serializable] public enum FileAttributes { ReadOnly = 0x00001, Hidden = 0x00002, System = 0x00004, Directory = 0x00010, Archive = 0x00020, Device = 0x00040, Normal = 0x00080, Temporary = 0x00100, SparseFile = 0x00200, ReparsePoint = 0x00400, Compressed = 0x00800, Offline = 0x01000, NotContentIndexed = 0x02000, Encrypted = 0x04000, IntegrityStream = 0x08000, NoScrubData = 0x20000 }
判断文件是否隐藏可执行以下代码:
String file = Assembly.GetEntryAssembly().Location; FileAttributes attributes = File.GetAttributes(file); Console.WriteLine("Is {0} hidden? {1}", file, (attributes & FileAttributes.Hidden) != 0);
HasFlag
Enum类定义了一个HasFlag方法:
public Boolean HasFlag(Enum flag);
可利用该方法重写上述Console.WriteLine调用:
Console.WriteLine("Is {0} hidden? {1}", file, attributes.HasFlag(FileAttributes.Hidden));
但我建议避免使用HasFlag方法,理由是:由于它获取Enum类型的参数,所以传给它的任何值都必须装箱,产生一次内存分配。
经常都要用枚举类型来表示一组可以组合的位标志。不过,虽然枚举类型和位标志相似,但它们的语义不尽相同。例如,枚举类型表示单个数值,而位标志表示位集合,其中一些位处于on状态,一些处于of状态。(进制1代表"on",二进制0代表"off")
定义用于标识位标志的枚举类型时,当然应该显式为每个符号分配一个数值。通常,每个符号都有单独的一个位处于on状态。此外,经常都要定义一个值为0的None符号。还可定义一些符合来代表常见的位组合(参见下面的ReadWrite符号)。另外,强烈建议向枚举类型应用定制特性类型System.FlagsAttribute。
[Flags] // The C# compiler allows either "Flags" or "FlagsAttribute". internal enum Actions { None = 0, Read = 0x0001, Write = 0x0002, ReadWrite = Actions.Read | Actions.Write, Delete = 0x0004, Query = 0x0008, Sync = 0x0010 }
Actions actions = Actions.Read | Actions.Delete; // 0x0005 Console.WriteLine(actions.ToString()); // "Read, Delete"
调用Tostring时,它会试图将数值转换为对应的符号。现在的数值是0x0005,没有对应的符号。不过,ToString方法检测到Actions类型上存在[Flags]特性,所以ToString方法现在不会将该数值视为单独的值。相反,会把它视为一组位标志。由于0x0005由0x0001和0x0004组合而成,所以ToString会生成字符串"Read,Delete"。从Actions类型中删除[Flags]特性,ToString方法将返回"5"
使用常规格式来格式化枚举类型的实例时,首先会检查类型,看它是否应用了Flags]这个特性。没有应用就查找与该数值匹配的符号并返回符号。如果应用了Flags1特性,ToString方法的工作过程如下所示。
- 获取枚举类型定义的数值集合,降序排列这些数值。
- 每个数值都和枚举实例中的值进行“按位与”计算,假如结果等于数值,与该数值关联的字符串就附加到输出字符串上,对应的位会被认为已经考虑过了,会被关闭(设为0)。这一步不断重复,直到检查完所有数值,或直到枚举实例的所有位都被关闭。
- 检查完所有数值后,如果校举实例仍然不为0,表明枚举实例中一些处于on状态的位不对应任何已定义的符号。在这种情况下,ToString将枚举实例中的原始数值作为字符串返回。
- 如果枚举实例原始值不为0,返回符号之间以逗号分隔的字符串。
- 如果枚举实例原始值为0,而且枚举类型定义的一个符号对应的是0值,就返回这个符号。
- 如果到达这一步,就返回"0"。
如果愿意,可定义没有[Flags]特性的Actions类型,并用"F"格式获得正确的字符串:
// [Flags] // Commented out now internal enum Actions { None = 0 Read = 0x0001, Write = 0x0002, ReadWrite = Actions.Read | Actions.Write, Delete = 0x0004, Query = 0x0008, Sync = 0x0010 } Actions actions = Actions.Read | Actions.Delete; // 0x0005 Console.WriteLine(actions.ToString("F")); // "Read, Delete"
如果数值有一个位不能映射到一个符号,返回的字符串只包含一个代表原始数值的十进制数;字符串中不会有符号。
注意,枚举类型中定义的符号不一定是2的整数次方。例如,Actions类型可定义一个名为All的符号,它对应的值是Ox001F(前面所有数值相加),如果Actions类型的一个实例的值是0x001F,格式化该实例就会生成一个含有"All"的字符串。其他符号字符串不会出现。
前面讨论的是如何将数值转换成标志字符串(string of flag),还可将以逗号分隔的符号字符串转换成数值,这是通过调用Enum的静态方法Parse和TryParse来实现的。
// Because Query is defined as 8, 'a' is initialized to 8. Actions a = (Actions) Enum.Parse(typeof(Actions), "Query", true); Console.WriteLine(a.ToString()); // "Query" // Because Query and Read are defined, 'a' is initialized to 9. Enum.TryParse<Actions>("Query, Read", false, out a); Console.WriteLine(a.ToString()); // "Read, Query" // Creates an instance of the Actions enum with a value of 28 a = (Actions) Enum.Parse(typeof(Actions), "28", false); Console.WriteLine(a.ToString()); // "Delete, Query, Sync"
Parse和TryParse方法在调用时,会在内部执行以下动作。
- 删除字符串头尾的所有空白字符。
- 如果字符串第一个字符是数字、加号(+)或减号(-),该字符串会被认为是一个数字,方法返回一个枚举类型实例,其数值等于字符串转换后的数值。
- 传递的字符串被分解为一组以逗号分隔的token,每个token的空白字符都被删除。
- 在枚举类型的已定义符号中查找每个token字符串。如果没有找到相应的符号,Parse会抛出System.ArgumentException异常;而TryParse会返回false。如果找到符号,就将它对应的数值与当前的一个动态结果进行“按位或”计算,再查找下一个符号。
- 查找并找到了所有标记之后,返回这个动态结果。
永远不要对位标志枚举类型使用1sDefined方法。以下两方面原因造成该方法无法使用。
- 向IsDefined方法传递字符串,它不会将这个字符串拆分为单独的token来进行查找,而是试图查找整个字符串,把它看成是包含逗号的一个更大的符号。由于不能在枚举类型中定义含有逗号的符号,所以这个符号永远找不到。
- 向IsDefined方法传递一个数值,它会检查枚举类型是否定义了其数值和传入数值匹配的一个符号。由于位标志不能这样简单地匹配,所以IsDefined通常会返回false.
15.3 向枚举类型添加方法
可以利用C#的扩展方法功能(参见第8章“方法”)模拟向枚举类型添加方法。
internal static class FileAttributesExtensionMethods { public static Boolean IsSet(this FileAttributes flags, FileAttributes flagToTest) { if (flagToTest == 0) throw new ArgumentOutOfRangeException("flagToTest", "Value must not be 0"); return (flags & flagToTest) == flagToTest; } public static Boolean IsClear(this FileAttributes flags, FileAttributes flagToTest) { if (flagToTest == 0) throw new ArgumentOutOfRangeException("flagToTest", "Value must not be 0"); return !IsSet(flags, flagToTest); } public static Boolean AnyFlagsSet(this FileAttributes flags, FileAttributes testFlags) { return ((flags & testFlags) != 0); } public static FileAttributes Set(this FileAttributes flags, FileAttributes setFlags) { return flags | setFlags; } public static FileAttributes Clear(this FileAttributes flags, FileAttributes clearFlags) { return flags & ~clearFlags; } public static void ForEach(this FileAttributes flags, Action<FileAttributes> processFlag) { if (processFlag == null) throw new ArgumentNullException("processFlag"); for (UInt32 bit = 1; bit != 0; bit <<= 1) { UInt32 temp = ((UInt32)flags) & bit; if (temp != 0) processFlag((FileAttributes)temp); } } }
浙公网安备 33010602011771号