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 case­sensitive 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方法的工作过程如下所示。

  1. 获取枚举类型定义的数值集合,降序排列这些数值。
  2. 每个数值都和枚举实例中的值进行“按位与”计算,假如结果等于数值,与该数值关联的字符串就附加到输出字符串上,对应的位会被认为已经考虑过了,会被关闭(设为0)。这一步不断重复,直到检查完所有数值,或直到枚举实例的所有位都被关闭。
  3. 检查完所有数值后,如果校举实例仍然不为0,表明枚举实例中一些处于on状态的位不对应任何已定义的符号。在这种情况下,ToString将枚举实例中的原始数值作为字符串返回。
  4. 如果枚举实例原始值不为0,返回符号之间以逗号分隔的字符串。
  5. 如果枚举实例原始值为0,而且枚举类型定义的一个符号对应的是0值,就返回这个符号。
  6. 如果到达这一步,就返回"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方法在调用时,会在内部执行以下动作。

  1. 删除字符串头尾的所有空白字符。
  2. 如果字符串第一个字符是数字、加号(+)或减号(-),该字符串会被认为是一个数字,方法返回一个枚举类型实例,其数值等于字符串转换后的数值。
  3. 传递的字符串被分解为一组以逗号分隔的token,每个token的空白字符都被删除。
  4. 在枚举类型的已定义符号中查找每个token字符串。如果没有找到相应的符号,Parse会抛出System.ArgumentException异常;而TryParse会返回false。如果找到符号,就将它对应的数值与当前的一个动态结果进行“按位或”计算,再查找下一个符号。
  5. 查找并找到了所有标记之后,返回这个动态结果。

永远不要对位标志枚举类型使用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);  
      } 
   } 
}
posted @ 2019-12-15 21:03  FH1004322  阅读(134)  评论(0)    收藏  举报