Enums

Compare the two code snippets shown in Listing 9.29.

Listing 9.29: Comparing an Integer Switch to 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—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.

Listing 9.30: Defining an Enum
enum ConnectionState
{
    Disconnected,
    Connecting,
    Connected,
    Disconnecting
}
note
An enum can be used as a more readable replacement for Boolean values as well. For example, a method call such as SetState(true) is less readable than SetState(DeviceState.On).

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.

Guidelines
DO NOT use the enum type name as part of the values name.
DO use an enum type name that is singular unless the enum is a flag.

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.

Listing 9.31: Defining an Enum Type
enum ConnectionState : short
{
    Disconnected,
    Connecting = 10,
    Connected,
    Joined = Connected,
    Disconnecting
}

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.

Guidelines
CONSIDER using the default 32-bit integer type as the underlying type of an enum. Use a smaller type only if you must do so for interoperability; use a larger type only if you are creating a flags enum10 with more than 32 flags.

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.

Guidelines
CONSIDER adding new members to existing enums, but keep in mind the compatibility risk.
AVOID creating enums that represent an “incomplete” set of values, such as product version numbers.
AVOID creating “reserved for future use” values in an enum.
AVOID enums that contain a single value.
DO provide a value of 0 (none) for simple enums, knowing that 0 will be the default value when no explicit initialization is provided.

AdVanced Topic
Type Compatibility between Enums

C# also does not support a direct cast between arrays of two different enums. However, the CLR does, provided that both enums share the same underlying type. To work around this restriction of C#, the trick is to cast first to System.Array, as shown at the end of Listing 9.32.

Listing 9.32: Casting between Arrays of Enums
enum ConnectionState1
{
    Disconnected,
    Connecting,
    Connected,
    Disconnecting
}
 
enum ConnectionState2
{
    Disconnected,
    Connecting,
    Connected,
    Disconnecting
}
public class Program
{
    public static void Main()
    {
        ConnectionState1[] states =
            (ConnectionState1[])(Array)new ConnectionState2[42];
    }
}

This example exploits the fact that the CLR’s notion of assignment compatibility is more lenient than C#’s concept. (The same trick is possible for other illegal conversions, such as from int[] to uint[].) However, you should use this approach cautiously, because no C# specification requires that this behavior work across different CLR implementations.

Converting between Enums and Strings

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:

Listing 9.33: Writing Enum Value Identifier to the Trace Buffer
System.Diagnostics.Trace.WriteLine(
    $"The connection is currently {ConnectionState.Disconnecting}");

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

Output 9.3
The connection is currently Disconnecting.

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.

Listing 9.34: Converting a String to an Enum Using Enum.TryParse()
System.Diagnostics.ThreadPriorityLevel priority;
if(Enum.TryParse("Idle"out priority))
{
    Console.WriteLine(priority);
}

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

Guidelines
AVOID direct enum/string conversions where the string must be localized into the user’s language.
Enums as Flags

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.

Listing 9.35: Using Enums as Flags
[Flags]
public enum FileAttributes
{
    None = 0,                       // 000000000000000
    ReadOnly = 1 << 0,              // 000000000000001
    Hidden = 1 << 1,                // 000000000000010
    System = 1 << 2,                // 000000000000100
    Directory = 1 << 4,             // 000000000010000
    Archive = 1 << 5,               // 000000000100000
    Device = 1 << 6,                // 000000001000000
    Normal = 1 << 7,                // 000000010000000
    Temporary = 1 << 8,             // 000000100000000
    SparseFile = 1 << 9,            // 000001000000000
    ReparsePoint = 1 << 10,         // 000010000000000
    Compressed = 1 << 11,           // 000100000000000
    Offline = 1 << 12,              // 001000000000000
    NotContentIndexed = 1 << 13,    // 010000000000000
    Encrypted = 1 << 14,            // 100000000000000
}
note
Note that the name of a bit flags enum is usually pluralized, indicating that a value of the type represents a set of flags.

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.

Listing 9.36: Using Bitwise OR and AND with Flag Enums12
using System;
using System.IO;
 
public class Program
{
    public static void Main()
    {
        // ...
 
        string fileName = @"enumtest.txt";
 
        // ...
            System.IO.FileInfo file = new(fileName);
 
            file.Attributes = FileAttributes.Hidden |
                FileAttributes.ReadOnly;
 
            Console.WriteLine($"{file.Attributes} = {(int)file.Attributes}");
 
            // Only the ReadOnly attribute works on Linux
            // (The Hidden attribute does not work on Linux)
            if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
            {
                // Added in C# 4.0/Microsoft .NET Framework  4.0
                if (!file.Attributes.HasFlag(FileAttributes.Hidden))
                {
                    throw new Exception("File is not hidden.");
                }
            }
 
            if ((file.Attributes & FileAttributes.ReadOnly) !=
            FileAttributes.ReadOnly)
            {
                throw new Exception("File is not read-only.");
            }
        // ...
    }
}
Output 9.4
Hidden | ReadOnly = 3

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.

Listing 9.37: Defining Enum Values for Frequent Combinations
[Flags]
enum DistributedChannel
{
    None = 0,
    Transacted = 1,
    Queued = 2,
    Encrypted = 4,
    Persisted = 16,
    FaultTolerant =
        Transacted | Queued | Persisted
}

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.

Guidelines
DO use the FlagsAttribute to mark enums that contain flag values.
DO provide a None value equal to 0 for all enums.
AVOID creating flag enums where the zero value has a meaning other than “no flags are set.”
CONSIDER providing special values for commonly used combinations of flags.
DO NOT include “sentinel” values (such as a value called Maximum); such values can be confusing to the user.
DO use powers of 2 to ensure that all flag combinations are represented uniquely.
AdVanced Topic
FlagsAttribute

If you decide to use bit flag enums, you should mark the declaration of the enum with FlagsAttribute. In such a case, the attribute appears in square brackets (see Chapter 18) just prior to the enum declaration, as shown in Listing 9.38 with Output 9.5.

Listing 9.38: Using FlagsAttribute
//FileAttributes are defined in System.IO
 
using System;
using System.IO;
 
public class Program
{
    public static void Main()
    {
        string fileName = @"enumtest.txt";
        // ...
        FileInfo file = new(fileName);
        file.Open(FileMode.OpenOrCreate).Dispose();
 
        FileAttributes startingAttributes =
            file.Attributes;
 
        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);
 
        File.SetAttributes(fileName,
            startingAttributes);
        file.Delete();
    }
}
Output 9.5
"ReadOnly | Hidden" outputs as "ReadOnly, Hidden"
ReadOnly, Hidden

The attribute 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 9.21, 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, you should use caution when relying on this behavior because it is not localizable.

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

Note that FlagsAttribute does not automatically assign unique flag values or check that they have unique values. Doing this wouldn’t make sense, given that duplicates and combinations are often desirable. Instead, you must assign the values of each enum item explicitly.

________________________________________

10. See the discussion in the section “Enums as Flags” later in this chapter.
11. Available starting in Microsoft .NET Framework 4.
12. Note that the FileAttributes.Hidden value does not work on Linux.
{{ snackbarMessage }}
;