# Chapter 15 Enumerated Types and Bit Flags

# Enumerated Types

An enumerated type is a type that defines a set of symbolic name and value pairs. For example, the Color type shown here defines a set of symbols, with each symbol identifying a single color.

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 
}

Of course, programmers can always write a program using 0 to represent white, 1 to represent red, and so on. However, programmers shouldn’t hard-code numbers into their code and should use an enumerated type instead, for at least two reasons:

  • Enumerated types make the program much easier to write, read, and maintain. With enumerated types, the symbolic name is used throughout the code, and the programmer doesn’t have to mentally map the meaning of each hard-coded value (for example, white is 0 or vice versa). Also, should a symbol’s numeric value change, the code can simply be recompiled without requiring any changes to the source code. In addition, documentation tools and other utilities, such as a debugger, can show meaningful symbolic names to the programmer.

  • Enumerated types are strongly typed. For example, the compiler will report an error if I attempt to pass Color.Orange as a value to a method requiring a Fruit enumerated type as a parameter.

In the Microsoft .NET Framework, enumerated types are more than just symbols that the compiler cares about. Enumerated types are treated as first-class citizens in the type system, which allows for very powerful operations that simply can’t be done with enumerated types in other environments (such as in unmanaged C++, for example).

Every enumerated type is derived directly from System.Enum , which is derived from System.ValueType , which in turn is derived from System.Object . So enumerated types are value types (described in Chapter 5, “Primitive, Reference, and Value Types”) and can be represented in unboxed and boxed forms. However, unlike other value types, an enumerated type can’t define any methods, properties, or events. However, you can use C#’s extension methods feature to simulate adding methods to an enumerated type. See the “Adding Methods to Enumerated Types” section at the end of this chapter for an example of this.

When an enumerated type is compiled, the C# compiler turns each symbol into a constant field of the type. For example, the compiler treats the Color enumeration shown earlier as if you had written code similar to the following.

internal struct Color : System.Enum { 
 // Following are public constants defining Color's symbols and values 
 public const Color White = (Color) 0; 
 public const Color Red = (Color) 1; 
 public const Color Green = (Color) 2; 
 public const Color Blue = (Color) 3; 
 public const Color Orange = (Color) 4; 
 // Following is a public instance field containing a Color variable's value 
 // You cannot write code that references this instance field directly 
 public Int32 value__; 
}

The C# compiler won’t actually compile this code because it forbids you from defining a type derived from the special System.Enum type. However, this pseudo-type definition shows you what’s happening internally. Basically, an enumerated type is just a structure with a bunch of constant fields defined in it and one instance field. The constant fields are emitted to the assembly’s metadata and can be accessed via reflection. This means that you can get all of the symbols and their values associated with an enumerated type at run time. It also means that you can convert a string symbol into its equivalent numeric value. These operations are made available to you by the System.Enum base type, which offers several static and instance methods that can be performed on an instance of an enumerated type, saving you the trouble of having to use reflection. I’ll discuss some of these operations next.

💡重要提示:枚举类型定义的符号时常量值。所以当编译器发现代码引用了枚举类型的符号时,会在编译时用数值替换符号,代码不再引用定义了符号的枚举类型。这意味着运行时可能并不不要定义了枚举类型的程序集,编译时才需要。假如代码引用了枚举类型 (而非仅仅引用类型定义的符号),那么运行时就需要包含了枚举类型定义的程序集。可能会出现一些版本问题,因为枚举类型符号是常量,而非只读的值。7.1 节 “常量” 已解释过这些问题。

For example, the System.Enum type has a static method called GetUnderlyingType , and the System.Type type has an instance method called GetEnumUnderlyingType .

public static Type GetUnderlyingType(Type enumType); // Defined in System.Enum
public Type GetEnumUnderlyingType(); // Defined in System.Type

These methods return the core type used to hold an enumerated type’s value. Every enumerated type has an underlying type, which can be a byte, sbyte, short, ushort, int (the most common type and what C# chooses by default), uint, long, or ulong. Of course, these C# primitive types correspond to FCL types. However, to make the implementation of the compiler itself simpler, the C# compiler requires you to specify a primitive type name here; using an FCL type name (such as Int32) generates the following message: error CS1008: Type byte, sbyte, short, ushort, int, uint, long, or ulong expected. The following code shows how to declare an enumerated type with an underlying type of byte ( System.Byte ).

internal enum Color : byte { 
 White, 
 Red, 
 Green, 
 Blue, 
 Orange 
}

With the Color enumerated type defined in this way, the following code shows what GetUnderlyingType will return.

// The following line displays "System.Byte". 
Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));

The C# compiler treats enumerated types as primitive types. As such, you can use many of the familiar operators (==, !=, <, >, <=, >=, +, -, ^, &, |, ~, ++, and --) to manipulate enumerated type instances. All of these operators actually work on the value__ instance field inside each enumerated type instance. Furthermore, the C# compiler allows you to explicitly cast instances of an enumerated type to a different enumerated type. You can also explicitly cast an enumerated type instance to a numeric type.

Given an instance of an enumerated type, it’s possible to map that value to one of several string representations by calling the ToString method inherited from System.Enum .

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 总是输出大写字母。此外,输出几位数取决于枚举的基础类型: byte/sbyte 输出 2 位数, short/ushort 输出 4 位数, int/uint 输出 8 位数,而 long/ulong 输出 16 位数。如有必要会添加前导零。

In addition to the ToString method, the System.Enum type offers a static Format method that you can call to format an enumerated type’s value.

public static String Format(Type enumType, Object value, String format);

Generally, I prefer to call the ToString method because it requires less code and it’s easier to call. But using Format has one advantage over ToString: Format lets you pass a numeric value for the value parameter; you don’t have to have an instance of the enumerated type. For example, the following code will display “Blue”.

// The following line displays "Blue". 
Console.WriteLine(Enum.Format(typeof(Color), (Byte)3, "G"));

💡注意:声明有多个符号的枚举类型时,所有符号都可以有相同的数值。使用常规格式将数值转换为符号时, Enum 的方法会返回其中一个符号,但不保证具体返回哪一个符号名称。另外,如果没有为要查找的数值定义符号,会返回包含该数值的字符串。

It’s also possible to call System.Enum ’s static GetValues method or System.Type ’s instance GetEnumValues method to obtain an array that contains one element for each symbolic name in an enumerated type; each element contains the symbolic name’s numeric value.

public static Array GetValues(Type enumType); // Defined in System.Enum
public Array GetEnumValues(); // Defined in System.Type

Using this method along with the ToString method, you can display all of an enumerated type’s symbolic and numeric values, like the following.

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); 
}

The previous code produces the following output.

Number of symbols defined: 5 
Value Symbol 
----- ----- 
 0 White 
 1 Red 
 2 Green 
 3 Blue 
 4 Orange

Personally, I don’t like the GetValues and GetEnumValues methods because they both return an Array, which I then have to cast to the appropriate array type. So, I always define my own method.

public static TEnum[] GetEnumValues<TEnum>() where TEnum : struct {
 return (TEnum[])Enum.GetValues(typeof(TEnum));
}

With my generic GetEnumValues method, I can get better compile-time type-safety and simplify the first line of code in the previous example to the following.

Color[] colors = GetEnumValues<Color>();

This discussion shows some of the cool operations that can be performed on enumerated types. I suspect that the ToString method with the general format will be used quite frequently to show symbolic names in a program’s user interface elements (list boxes, combo boxes, and the like), as long as the strings don’t need to be localized (because enumerated types offer no support for localization). In addition to the GetValues method, the System.Enum type and the System.Type type also offer the following methods that return an enumerated type’s symbols.

// 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

I’ve discussed a lot of methods that you can use to look up an enumerated type’s symbol. But you also need a method that can look up a symbol’s equivalent value, an operation that could be used to convert a symbol that a user enters into a text box, for example. Converting a symbol to an instance of an enumerated type is easily accomplished by using one of Enum ’s static Parse and TryParse methods.

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;

Here’s some code demonstrating how to use this method.

// 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);

Finally, using the following Enum’s static IsDefined method and Type’s IsEnumDefined method:

public static Boolean IsDefined(Type enumType, Object value); // Defined in System.Enum
public Boolean IsEnumDefined(Object value); // Defined in System.Type
you can determine whether a numeric value is legal for an enumerated type.
// 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));

The IsDefined method is frequently used for parameter validation. Here’s an example.

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 
 ... 
}

The parameter validation is useful because someone could call SetColor like the following.

SetColor((Color) 547);

Because no symbol has a corresponding value of 547, the SetColor method will throw an ArgumentOutOfRangeException exception, indicating which parameter is invalid and why.

💡重要提示: IsDefined 方法很方便,但必须慎用。首先, IsDefined 总是执行区分大小写的查找,而且完全没有办法让它执行不区分大小写的查找。其次, IsDefined 相当慢,因为它在内部使用了反射。如果写代码来手动检查每一个可能的值,应用程序的性能极有可能变得更好。最后,只有当枚举类型本身在调用 IsDefined 的同一个程序集中定义, SetColor 方法在另一个程序集中定义。 SetColor 方法调用 IsDefined ,假如颜色是 WhiteRed , Green , Blue 或者 Orange ,那么 SetColor 能正常执行。然而,假如 Color 枚举将来发生了变化,在其中包含了 Purple ,那么 SetColor 现在就会接受 Purple ,这是以前没有预料到的。因此,方法现在可能返回无法预料的结果。

Finally, the System.Enum type offers a set of static ToObject methods that convert an instance of a Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, or UInt64 to an instance of an enumerated type.

Enumerated types are always used in conjunction with some other type. Typically, they’re used for the type’s method parameters or return type, properties, and fields. A common question that arises is whether to define the enumerated type nested within the type that requires it or to define the enumerated type at the same level as the type that requires it. If you examine the FCL, you’ll see that an enumerated type is usually defined at the same level as the class that requires it. The reason is simply to make the developer’s life a little easier by reducing the amount of typing required. So you should define your enumerated type at the same level unless you’re concerned about name conflicts.

💡小结:枚举类型(enumerated type)定义了一组 “符号名称 / 值” 配对。在 Microsoft.NET Framework 中,枚举类型不只是编译器所关心的符号,它还是类型系统中的 “一等公民”,能实现很强大的操作。每个枚举类型都直接从 System.Enum 派生,后者从 System.ValueType 派生,而 System.ValueType 又从 System.Object 派生。所以,枚举类型是值类型,可用未装箱和已装箱的形式来表示。但有别于其他值类型,枚举类型不能定义任何方法、属性或事件。不过,可利用 C# 的 “扩展方法” 功能模拟向枚举类型添加方法。简单地说,枚举类型只是一个结构,其中定义了一组常量字段和一个实例字段。常量字段会嵌入程序集的元数据中,并可通过反射来访问。这意味着可以在运行时获得与枚举类型关联的所有符号及其值。 System.Enum 类型有一个名为 GetUnderlying 的静态方法,而 System.Type 类型有一个名为 GetEnumUnderlyingType 的实例方法。这些方法返回用于容纳一个枚举类型的值的基础类型。每个枚举类型都有一个基础类型,它可以是 byte,sbyte,short,ushort,int(最常用,也是 C# 默认选择的),uint,long 或 ulong。虽然这些 C# 基元类型都有对应的 FCL 类型,但 C# 编译器为了简化本身的实现,要求只能指定基元类型名称。不要混淆 “基础类型”(underlying type)和 “基元类型”(primitive type)。虽然枚举的基础类型就是这些基元类型(其实就是除 Char 之外的所有整型),但英语中用 underlying type 和 primitive type 进行了区分,中文翻译同样要区分。C# 编译器将枚举类型视为基元类型。所以可用许多熟悉的操作符(==,!=,<,>,<=,>=,+,-,^,&,|,~,++ 和 --)来操纵枚举类型的实例。所有这些操作符实际作用于每个枚举类型实力内部的 value__ 实例字段。此外,C# 编译器允许将枚举类型的实例显式转型为不同的枚举类型。也可显式将枚举类型实例转型为数值类型。检查 FCL,会发现枚举类型通常与需要它的类同级。原因很简单,就是减少代码的录入量,使开发人员的工作变得更轻松。所以,除非担心名称冲突,否则你定义的枚举类型应该和需要它的类型同级。

# Bit Flags

Programmers frequently work with sets of bit flags. When you call the System.IO.File type’s GetAttributes method, it returns an instance of a FileAttributes type. A FileAttributes type is an instance of an Int32-based enumerated type, in which each bit reflects a single attribute of the file. The FileAttributes type is defined in the FCL as follows.

[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 
}

To determine whether a file is hidden, you would execute code like the following.

String file = Assembly.GetEntryAssembly().Location;
FileAttributes attributes = File.GetAttributes(file); 
Console.WriteLine("Is {0} hidden? {1}", file, (attributes & FileAttributes.Hidden) != 0);

💡注意: Enum 类定义了一个 HasFlag 方法:

public Boolean HasFlag(Enum flag);

可利用该方法重写上述 Console.WriteLine 调用:

Console.WriteLine("Is {0} hidden? {1}", file, attributes.HasFlag(FileAttributes.Hidden));

但我建议避免使用 HasFlag 方法,理由是:由于它获取 Enum 类型的参数,所以传给它的任何值都必须装箱,产生一次内存分配。

And here’s code demonstrating how to change a file’s attributes to read-only and hidden.

File.SetAttributes(file, FileAttributes.ReadOnly | FileAttributes.Hidden);

As the FileAttributes type shows, it’s common to use enumerated types to express the set of bit flags that can be combined. However, although enumerated types and bit flags are similar, they don’t have exactly the same semantics. For example, enumerated types represent single numeric values, and bit flags represent a set of bits, some of which are on, and some of which are off.

When defining an enumerated type that is to be used to identify bit flags, you should, of course, explicitly assign a numeric value to each symbol. Usually, each symbol will have an individual bit turned on. It is also common to see a symbol called None defined with a value of 0, and you can also define symbols that represent commonly used combinations (see the following ReadWrite symbol). It’s also highly recommended that you apply the System.FlagsAttribute custom attribute type to the enumerated type, as shown here:

[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 
}

Because Actions is an enumerated type, you can use all of the methods described in the previous section when working with bit-flag enumerated types. However, it would be nice if some of those functions behaved a little differently. For example, let’s say you had the following code.

Actions actions = Actions.Read | Actions.Delete; // 0x0005 
Console.WriteLine(actions.ToString()); // "Read, Delete"

When ToString is called, it attempts to translate the numeric value into its symbolic equivalent. The numeric value is 0x0005, which has no symbolic equivalent. However, the ToString method detects the existence of the [Flags] attribute on the Actions type, and ToString now treats the numeric value not as a single value but as a set of bit flags. Because the 0x0001 and 0x0004 bits are set, ToString generates the following string: "Read, Delete". If you remove the [Flags] attribute from the Actions type, ToString would return “5”.

I discussed the ToString method in the previous section, and I showed that it offered three ways to format the output: “G” (general), “D” (decimal), and “X” (hex). When you’re formatting an instance of an enumerated type by using the general format, the type is first checked to see if the [Flags] attribute is applied to it. If this attribute is not applied, a symbol matching the numeric value is looked up and returned. If the [Flags] attribute is applied, ToString works like this:

  1. The set of numeric values defined by the enumerated type is obtained, and the numbers are sorted in descending order.
  2. Each numeric value is bitwise-ANDed with the value in the enum instance, and if the result equals the numeric value, the string associated with the numeric value is appended to the output string, and the bits are considered accounted for and are turned off. This step is repeated until all numeric values have been checked or until the enum instance has all of its bits turned off.
  3. If, after all the numeric values have been checked, the enum instance is still not 0, the enum instance has some bits turned on that do not correspond to any defined symbols. In this case, ToString returns the original number in the enum instance as a string.
  4. If the enum instance’s original value wasn’t 0, the string with the comma-separated set of symbols is returned.
  5. If the enum instance’s original value was 0 and if the enumerated type has a symbol defined with a corresponding value of 0, the symbol is returned.
  6. If we reach this step, “0” is returned.

If you prefer, you could define the Actions type without the [Flags] attribute and still get the correct string by using the “F” format.

// [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"

If the numeric value has a bit that cannot be mapped to a symbol, the returned string will contain just a decimal number indicating the original numeric value; no symbols will appear in the string.

Note that the symbols you define in your enumerated type don’t have to be pure powers of 2. For example, the Actions type could define a symbol called All with a value of 0x001F. If an instance of the Actions type has a value of 0x001F, formatting the instance will produce a string that contains “All”. The other symbol strings won’t appear.

So far, I’ve discussed how to convert numeric values into a string of flags. It’s also possible to convert a string of comma-delimited symbols into a numeric value by calling Enum’s static Parse and TryParse method. Here’s some code demonstrating how to use this method.

// 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"

When Parse and TryParse are called, the following actions are performed internally:

  1. It removes all whitespace characters from the start and end of the string.
  2. If the first character of the string is a digit, plus sign (+), or minus sign (-), the string is assumed to be a number, and an enum instance is returned whose numeric value is equal to the string converted to its numeric equivalent.
  3. The passed string is split into a set of tokens (separated by commas), and all white space is trimmed away from each token.
  4. Each token string is looked up in the enum type’s defined symbols. If the symbol is not found, Parse throws a System.ArgumentException and TryParse returns false. If the symbol is found, bitwise-OR its numeric value into a running result, and then look up the next token.
  5. If all tokens have been sought and found, return the running result.

You should never use the IsDefined method with bit flag–enumerated types. It won’t work for two reasons:

  • If you pass a string to IsDefined, it doesn’t split the string into separate tokens to look up; it will attempt to look up the string as through it were one big symbol with commas in it. Because you can’t define an enum with a symbol that has commas in it, the symbol will never be found.
  • If you pass a numeric value to IsDefined, it checks whether the enumerated type defines a single symbol whose numeric value matches the passed-in number. Because this is unlikely for bit flags, IsDefined will usually return false.

💡小结:调用 System.IO.File 类型的 GetAttributes 方法,会返回 FileAttributes 类型的一个实例。 FileAttributes 类型是基本类型为 Int32 的枚举类型,其中每一位都反映了文件的一个特性(attribute)。经常都要用枚举类型来表示一组可以组合的位标志。虽然枚举类型和位标志相似,但它们的语义不尽相同。例如,枚举类型表示单个数值,而位标志表示位集合,其中一些位处于 on 状态,一些处于 off 状态。强烈建议向枚举类型应用定制特性类型 System.FlagsAttributeToString 方法检测到类型上存在 [Flags] 特性就不会把数值视为单独的值,而是把它视为一组位标志。 ToString 方法允许以 3 种方式格式化输出:“G”(常规)、“D”(十进制)和 “X”(十六进制)。使用常规格式来格式化枚举类型的实例时,首先会检查类型,看它是否应用了 [Flags] 这个特性。没有应用就查找与该数值匹配的符号并返回符号。如果应用了 [Flags] 特性, ToString 方法会降序排序枚举类型定义的数值集合。每个数值都和枚举实例中的值进行 “按位与” 计算,假如计算结果等于数值,与该数值关联的字符串就会附加到输出字符串上,对应的位会被认为已经考虑过了,会被关闭(设为 0)。这一步不断重复,直到检查完所有数值,或直到枚举实例的所有位都被关闭。检查玩所有数值后,如果枚举实例仍然不为 0,表示枚举实例中一些处于 on 状态的位不对应任何已定义的符号。在这种情况下, ToString 将枚举实例中原始数值作为字符串返回。即如果数值有一个位不能映射到一个符号,返回的字符串只包含一个代表原始数值的十进制数;字符串中不会有符号。永远不要对位标志枚举类型使用 IsDefined 方法。向 IsDefined 方法传递字符串,它不会将这个字符串拆分位单独的 token 来进行查找,而是试图查找整个字符串,把它看成是包含逗号的一个更大的符号。向 IsDefined 方法传递一个数值,它会检查枚举类型是否定义了其数值和传入数值匹配的一个符号。由于位标志不能这样简单地匹配,所以 IsDefined 通常会返回 false。

# Adding Methods to Enumerated Types

Earlier in this chapter, I mentioned that you cannot define a method as part of an enumerated type. And, for many years, this has saddened me because there are many occasions when I would love to have been able to supply some methods to my enumerated type. Fortunately, I can use C#’s extension method feature (discussed in Chapter 8, “Methods”) to simulate adding methods to an enumerated type.

If I want to add some methods to the FileAttributes enumerated type, I can define a static class with extension methods as follows.

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);
 }
 }
}

And here is some code that demonstrates calling some of these methods. As you can see, the code looks as if I’m calling methods on the enumerated type.

FileAttributes fa = FileAttributes.System;
fa = fa.Set(FileAttributes.ReadOnly);
fa = fa.Clear(FileAttributes.System);
fa.ForEach(f => Console.WriteLine(f));

💡小结:可以利用 C# 的扩展方法功能模拟向枚举类型添加方法。