Enums


Compare the two code snippets shown in Listing 8.9.

Listing 8.9. Comparing an Integer with an Enum Switch

int connectionState; // ... switch (connectionState) {     case 0:         // ...         break;        case 1:         // ...         break;        case 2:                // ...                break;        case 3:        // ...        break; } ConnectionState connectionState; // ... switch (connectionState) {     case ConnectionState.Connected:         // ...         break;     case ConnectionState.Connecting:         // ...         break;     case ConnectionState.Disconnected:         // ...         break;     case ConnectionState.Disconnecting:         // ...         break; }

Obviously, the difference in terms of readability is tremendous because in the second snippet, the cases are self-documenting to some degree. However, the performance at runtime is identical. To achieve this, the second snippet uses enum values in each case statement.

An enum is a type that the developer can define. The key characteristic of an enum is that it identifies a compile-time-defined set of possible values, each value referred to by name, making the code easier to read. You define an enum using a style similar to that for a class, as Listing 8.10 shows.

Listing 8.10. Defining an Enum

enum ConnectionState {   Disconnected,   Connecting,   Connected,   Disconnecting }

Note

An enum is helpful even for Boolean parameters. For example, a method call such as SetState(true) is less readable than SetState(DeviceState.On).


You refer to an enum value by prefixing it with the enum name; to refer to the Connected value, for example, you use ConnectionState.Connected. You should not use the enum names within the enum value name, to avoid the redundancy of something like ConnectionState.ConnectionStateConnected. By convention, the enum name itself should be singular, unless the enums are bit flags (discussed shortly).

By default, the first enum value is 0 (technically, it is 0 explicitly cast to the underlying enum type), and each subsequent entry increases by one. However, you can assign explicit values to enums, as shown in Listing 8.11.

Listing 8.11. Defining an Enum Type

enum ConnectionState : short {   Disconnected,   Connecting = 10,   Connected,   Joined = Connected,   Disconnecting }

Disconnected has a default value of 0, Connecting has been explicitly assigned 10, and consequently, Connected will be assigned 11. Joined is assigned 11, the value referred to by Connected. (In this case, you do not need to prefix Connected with the enum name, since it appears within its scope.) Disconnecting is 12.

An enum always has an underlying type, which may be any of the integral types, except for char. In fact, the enum type's performance is equivalent to that of the underlying type. By default, the underlying value type is int, but you can specify a different type using inheritance type syntax. Instead of int, for example, Listing 8.11 uses a short. For consistency, the syntax emulates that of inheritance, but this doesn't actually make an inheritance relationship. The base class for all enums is System.Enum. Furthermore, these classes are sealed; you can't derive from an existing enum type to add additional members.

Successful conversion doesn't work just for valid enum values. It is possible to cast 42 into a ConnectionState, even though there is no corresponding ConnectionState enum value. If the value successfully casts to the underlying type, the conversion will be successful.

The advantage to allowing casting, even without a corresponding enum value, is that enums can have new values added in later API releases, without breaking earlier versions. Additionally, the enum values provide names for the known values while still allowing unknown values to be assigned at runtime. The burden is that developers must code defensively for the possibility of unnamed values. It would be unwise, for example, to replace case ConnectionState.Disconnecting with default and expect that the only possible value for the default case was ConnectionState.Disconnecting. Instead, you should handle the Disconnecting case explicitly and the Default case should report an error or behave innocuously. As indicated before, however, conversion between the enum and the underlying type, and vice versa, involves an explicit cast, not an implicit one. For example, code cannot call ReportState(10) where the signature is void ReportState(ConnectionState state). (The only exception is passing 0 because there is an implicit cast from 0 to any enum.) The compiler will perform a type check and require an explicit cast if the type is not identical.

Although you can add additional values to an enum in a later version of your code, you should do this with care. Inserting an enum value in the middle of an enum will bump the values of all later enums (adding Flooded or Locked before Connected will change the Connected value, for example). This will affect the versions of all code that is recompiled against the new version. However, any code compiled against the old version will continue to use the old values, making the intended values entirely different. Besides inserting an enum value at the end of the list, one way to avoid changing enum values is to assign values explicitly.

Enums are slightly different from other value types because enums derive from System.Enum before deriving from System.ValueType.

Type Compatibility between Enums

Chapter 6 discussed that C# does not support covariance (type compatibility) between arrays of derived types. For example, Contact[] will not cast to PdaItem[]. C# also does not support a direct cast between arrays of two different enums. However, there is a way to coerce the conversion by casting first to an array and then to the second enum. The requirement is that both enums share the same underlying type, and the trick is to cast first to System.Array, as shown at the end of Listing 8.12.

Listing 8.12. Casting between Arrays of Enums

enum ConnectionState1 {   Disconnected,   Connecting,   Connected,   Disconnecting } ___________________________________________________________________________ ___________________________________________________________________________ enum ConnectionState2 {   Disconnected,   Connecting,   Connected,   Disconnecting } ___________________________________________________________________________ ___________________________________________________________________________ class Program {   static void Main()   {       ConnectionState1[] states =           (ConnectionState2[]) (Array) new ConnectionState2[42];   } }

Converting between Enums and Strings

One of the conveniences associated with enums is the fact that the ToString() method, which is called by methods like System.Console.WriteLine(), writes out the enum value identifier:

System.Diagnostics.Trace.WriteLine(string.Format(     "The Connection is currently {0}.",     ConnectionState.Disconnecting));


The preceding code will write the text in Output 8.3 to the trace buffer.

Output 8.3.

The Connection is currently Disconnecting.

Conversion from a string to an enum is a little harder to find because it involves a static method on the System.Enum base class or, via inheritance, on the enum type. Listing 8.13 provides an example of how to do it, and Output 8.4 shows the results.

Listing 8.13. Converting a String to an Enum

ThreadPriorityLevel priority = (ThreadPriorityLevel)Enum.Parse(   typeof(ThreadPriorityLevel), "Idle"); Console.WriteLine(priority);

Output 8.4.

Idle

The first parameter to Enum.Parse() is the type, which you specify using the keyword typeof(). This is a compile-time way of identifying the type, like a literal for the type value (see Chapter 14).

Unfortunately, there is no tryParse() method, so code should include appropriate exception handling if there is a chance the string will not correspond to an enum value identifier. The key caution about casting from a string to an enum, however, is that such a cast is not localizable. Therefore, developers should use this type of cast only for messages that are not exposed to users (assuming localization is a requirement).

Enums as Flags

Many times, developers not only want enum values to be unique, but they also want to be able to combine them to represent a combinatorial value. For example, consider System.IO.FileAttributes. This enum, shown in Listing 8.14, indicates various attributes on a file: read-only, hidden, archive, and so on. The difference is that unlike the ConnectionState attribute, where each enum value was mutually exclusive, the FileAttributes enum values can and are intended for combination: A file can be both read-only and hidden. To support this, each enum value is a unique bit (or a value that represents a particular combination).

Listing 8.14. Using Enums As Flags

public enum FileAttributes {   ReadOnly =          2^0,       // 000000000000001   Hidden =            2^1,       // 000000000000010   System =            2^2,       // 000000000000100   Directory =         2^4,       // 000000000010000   Archive =           2^5,       // 000000000100000   Device =            2^6,       // 000000001000000   Normal =            2^7,       // 000000010000000   Temporary =         2^8,       // 000000100000000   SparseFile =        2^9,       // 000001000000000   ReparsePoint =      2^10,      // 000010000000000   Compressed =        2^11,      // 000100000000000   Offline =           2^12,      // 001000000000000   NotContentIndexed = 2^13,      // 010000000000000   Encrypted =         2^14,      // 100000000000000 }

Because enums support combined values, the guideline for the enum name of bit flags is plural.

To join enum values you use a bitwise OR operator, as shown in Listing 8.15.

Listing 8.15. Using Bitwise OR and AND with Flag Enums

using System; using System.IO; public class Program {  public static void Main()  {      // ...      string fileName = @"enumtest.txt";      System.IO.FileInfo file = new System.IO.FileInfo(fileName);         file.Attributes = FileAttributes.Hidden |             FileAttributes.ReadOnly;         Console.WriteLine("{0} | {1} = {2}",             FileAttributes.Hidden, FileAttributes.ReadOnly,             (int)file.Attributes);         if ( (file.Attributes & FileAttributes.Hidden) !=             FileAttributes.Hidden)         {             throw new Exception("File is not hidden.");         }         if (( file.Attributes & FileAttributes.ReadOnly) !=              FileAttributes.ReadOnly)         {              throw new Exception("File is not read-only.");         }         // ... }

The results of Listing 8.15 appear in Output 8.5.

Output 8.5.

Hidden | ReadOnly = 3

Using the bitwise OR operator allows you to set the file to both read-only and hidden. In addition, you can check for specific settings using the bitwise AND operator.

Each value within the enum does not need to correspond to only one flag. It is perfectly reasonable to define additional flags that correspond to frequent combinations of values. Listing 8.16 shows an example.

Listing 8.16. Defining Enum Values for Frequent Combinations

enum DistributedChannel {   Transacted = 1,   Queued = 2,   Encrypted = 4,   Persisted = 16, FaultTolerant =                                                                      Transacted | Queued | Persisted                                    }

However, you should avoid enum values corresponding to things like Maximum as the last enum, because Maximum could be interpreted as a valid enum value. To check whether a value is included within an enum use the System.Enum.IsDefined() method.

Advanced Topic: FlagsAttribute

If you decide to use flag-type values, the enum should include FlagsAttribute. The attribute appears in square brackets (see Chapter 14), just prior to the enum declaration, as shown in Listing 8.17.

Listing 8.17. Using FlagsAttribute

 // FileAttributes defined in System.IO. [Flags] // Decorating an enum with FlagsAttribute.            public enum FileAttributes {    ReadOnly =        2^0,          // 000000000000001    Hidden =          2^1,          // 000000000000010    // ... } using System; using System.Diagnostics; using System.IO; class Program {   public static void Main()   {         // ...         string fileName = @"enumtest.txt";         FileInfo file = new FileInfo(fileName);         file.Attributes = FileAttributes.Hidden |                 FileAttributes.ReadOnly;         Console.WriteLine("\"{0}\" outputs as \"{1}\"",                 file.Attributes.ToString().Replace(",", " |"),                 file.Attributes);         FileAttributes attributes =                 (FileAttributes) Enum.Parse(typeof(FileAttributes),                 file.Attributes.ToString());         Console.WriteLine(attributes);           // ...   } }

The results of Listing 8.17 appear in Output 8.6.

Output 8.6.

"ReadOnly | Hidden" outputs as "ReadOnly, Hidden"

The flag documents that the enum values can be combined. Furthermore, it changes the behavior of the ToString() and Parse() methods. For example, calling ToString() on an enum that is decorated with FlagsAttribute writes out the strings for each enum flag that is set. In Listing 8.17, file.Attributes.ToString() returns ReadOnly, Hidden rather than the 3 it would have returned without the FileAttributes flag. If two enum values are the same, the ToString() call would return the first value. As mentioned earlier, however, you should use this with caution because it is not localizable.

Parsing a value from a string to the enum also works. Each enum value identifier is separated by a comma.

It is important to note that FlagsAttribute does not automatically assign unique flag values or check that they have unique values. Doing this wouldn't make sense, since duplicates and combinations are often desirable. Instead, you must assign the values of each enum item explicitly.





Essential C# 2.0
Essential C# 2.0
ISBN: 0321150775
EAN: 2147483647
Year: 2007
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net