Compare the two code snippets shown in Listing 9.29.
Obviously, the difference in terms of readability is tremendous—in the second snippet, the cases are self-documenting. However, the performance at runtime is identical. To achieve this outcome, the second snippet uses enum values.
An enum is a value type that the developer can declare. The key characteristic of an enum is that it declares, at compile time, a set of possible constant values that can be referred to by name, thereby making the code easier to read. The syntax for a typical enum declaration is shown in Listing 9.30.
You use an enum value by prefixing it with the enum’s name. To use the Connected value, for example, you would use the syntax ConnectionState.Connected. Do not make the enum type name a part of the value’s name to avoid the redundancy of ConnectionState.ConnectionStateConnected and similar references. By convention, the enum name itself should be singular (unless the enum values are bit flags, as discussed shortly). That is, the nomenclature should be ConnectionState, not ConnectionStates.
Enum values are actually implemented as nothing more than integer constants. By default, the first enum value is given the value 0, and each subsequent entry increases by 1. Alternatively, you can assign explicit values to enum members, as shown in Listing 9.31.
In this code, Disconnected has a default value of 0 and Connecting has been explicitly assigned 10; consequently, Connected will be assigned 11. Joined is assigned 11, the value assigned to 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 integral type other than char. In fact, the enum type’s performance is identical 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 9.15 uses a short. For consistency, the syntax for enums emulates the syntax of inheritance, but it doesn’t actually create an inheritance relationship. The base class for all enums is System.Enum, which in turn is derived from System.ValueType. Furthermore, these enums are sealed; you can’t derive from an existing enum type to add more members.
An enum is really nothing more than a set of names thinly layered on top of the underlying type; there is no mechanism that restricts the value of an enumerated type variable to just the values named in the declaration. For example, because it is possible to cast the integer 42 to short, it is also possible to cast the integer 42 to the ConnectionState type, even though there is no corresponding ConnectionState enum value. If the value can be converted to the underlying type, the conversion to the enum type will also be successful.
The advantage of this odd feature 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 earlier, however, conversion between the enum and the underlying type, and vice versa, requires an explicit cast; it is not an implicit conversion. For example, code cannot call ReportState(10) if the method’s signature is void ReportState(ConnectionState state). The only exception occurs when passing 0, because there is an implicit conversion from 0 to any enum.
Although you can add more values to an enum in a later version of your code, you should do so with care. Inserting an enum value in the middle of an enum will bump the values of all later enum values (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.
One of the conveniences associated with enums is that the ToString() method, which is called by methods such as System.Console.WriteLine(), writes out the enum value identifier as shown in Listing 9.33:
The preceding code will write the text in Output 9.3 to the trace buffer.
Conversion from a string to an enum is a little less obvious because it involves a static method on the System.Enum base class. Listing 9.34 provides an example of how to do it without generics (see Chapter 12). “Idle” appears as the output.
This example depicts a compile-time way of identifying the type, like a literal for the type value (see Chapter 18). The TryParse()11 method (technically TryParse<T>()) uses generics, but the type parameters can be inferred, resulting in the to-enum conversion syntax shown in Listing 9.17. There is also an equivalent Parse() method that throws an exception if it fails, making it suboptimal unless success is certain.
Regardless of whether the code uses the “Parse” or “TryParse” approach, the key caution about converting from a string to an enum 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).
Many times, developers not only want enum values to be unique but also want to be able to represent a combination of values. For example, consider System.IO.FileAttributes. This enum, shown in Listing 9.35, indicates various attributes of a file: read-only, hidden, archive, and so on. Unlike with the ConnectionState attribute, where each enum’s values were mutually exclusive, the FileAttributes enum values can be, and are, intended for combination: A file can be both read-only and hidden. To support this behavior, each enum member’s value is a unique bit.
To join enum values, you use a bitwise OR operator. To test for the existence of a particular flag, use the Enum.HasFlags() method or use the bitwise AND operator. Both cases are illustrated in Listing 9.36 with Output 9.4.
Using the bitwise OR operator allows you to set the file attributes to both read-only and hidden.
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 9.37 shows an example.
It is a good practice to have a zero None member in a flags enum because the initial default value of a field of enum type or an element of an array of enum type is 0. Avoid enum values corresponding to items such as 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.