Many program languages support a concept called Enumerated Types or better known as Enums. An Enum is a set of named constants that can be used as a type. C# does also support Enums, and the construct is easy to use. But Enums have a few pitfalls that are not obvious and can lead to unexpected behavior. Not obvious is also, that C# supports two different kinds of Enums with slight differences.
How to define an Enum?
An Enum is usually defined in a separate file, like a class
. The name should be in singular.
namespace SomeNamespace
{
public enum PlanetCategory
{
TerrestrialPlanet,
GasGiant,
IceGiant
}
}
The members can have an integral numeric value if needed:
namespace SomeNamespace
{
public enum PlanetCategory
{
TerrestrialPlanet = 1,
GasGiant = 200,
IceGiant = 3333
}
}
An Enum is represented by an Int32
value, but it is possible to define any
integral numeric type. This can be used, if the represented values don’t fall in the
Int32
range or you need a different type for interoperability with unmanaged code:
namespace SomeNamespace
{
public enum PlanetCategory : long
{
TerrestrialPlanet = 1,
GasGiant = 2,
IceGiant = long.MaxValue
}
}
If you don’t need the numeric values for storing or serialization, it is recommended to omit the values. Even if you omit the numeric values, the compiler will assign a value, starting from 0 and increasing by 1.
In cases where you set values, you should define a value for 0 because the Enum is always initialized with 0 (more on that later). The values don’t have to be unique, but duplicates should be avoided, and the compiler shows a warning for those cases.
How to define a Flags Enum?
As already mentioned in the intro, there are two kinds of Enums with slight differences.
The second kind is for flags/bitmasks and can be used if multiple members of the Enum can
apply. The declaration looks like a regular Enum but with an additional [Flags]
attribute on the Enum. Unlike regular Enums, Flags Enums should be named in plural.
One important part of Flags Enums are the numeric values of the members. A Flags Enum without
numeric values does not make much sense. Also, it is recommended to provide a member called
None
with the value 0. Since the values can be combined, a numeric value should
only represent one bit. This means, the values should not increase by 1, but by the power of 2,
although combinations of values are possible. One way to simplify this is to use binary literals:
[Flags]
public enum PlanetFeatures
{
None = 0b0000_0000, // 0
HasHumans = 0b0000_0001, // 1
HasMoons = 0b0000_0010, // 2
HasRings = 0b0000_0100, // 4
HasHumansAndMoons = 0b0000_0011, // 3 (combination)
}
Another option is to use the left-shift operator:
[Flags]
public enum PlanetFeatures
{
None = 0, // 0
HasHumans = 1 << 0, // 1
HasMoons = 1 << 1, // 2
HasRings = 1 << 2, // 4
}
Bit manipulations
can be used to work with Flags Enums. To combine multiple members, we use the bitwise OR operator
|
. To check if a flag was set, we combine the given value with the value to check with
the bitwise AND operator &
and check if this value is equal to the value to check.
The HasFlag()
method simplifies the check for a single flag.
PlanetFeatures features = PlanetFeatures.HasMoons | PlanetFeatures.HasRings;
bool hasHumans = ((features & PlanetFeatures.HasHumans) == PlanetFeatures.HasHumans); // false
bool hasMoons = features.HasFlag(PlanetFeatures.HasMoons); // true
The [Flags]
attribute isn’t required, but it helps to document the usage
of the Enum. Another benefit is the behavior of the ToString()
method.
With the [Flags]
attribute the ToString()
method (which is
also used for the debugger view) of the features variable in the sample above would
return “HasMoons, HasRings”, without the [Flags]
attribute it would be “6”.
And what can lead to unexpected behavior?
An Enum simplifies the definition of possible values. But in the end, an Enum is still just an int and can be set to an arbitrary int value:
public enum PlanetCategory
{
TerrestrialPlanet,
GasGiant,
IceGiant
}
// set with Enum value
PlanetCategory category1 = PlanetCategory.TerrestrialPlanet;
string category1Value = category1.ToString(); // "TerrestrialPlanet
// set with valid int value (in this case the value is implicitly defined by position)
PlanetCategory category2 = (PlanetCategory)1;
string category2Value = category2.ToString(); // "GasGiant"
// set with an arbitrary int is also possible
PlanetCategory category3 = (PlanetCategory)99999;
string category3Value = category3.ToString(); // "99999"
Another pitfall is the default value of Enums, which is always 0, even if you don’t define a member with this value:
public enum PlanetCategory
{
TerrestrialPlanet = 1,
GasGiant = 2,
IceGiant = 3
}
PlanetCategory category = default(PlanetCategory);
int categoryValue = (int)category; // categoryValue is 0
string categoryName = category.ToString(); // categoryName is "0"
To catch Enum values that are not defined, we can use the Enum.IsDefined()
method. This method returns true
, if the given value is defined and
false
, if not. But beware, that this method does also return false
,
if the Enum has the [Flags]
attribute and the value is a valid combination
of multiple members:
PlanetCategory category1 = PlanetCategory.IceGiant;
bool category1IsDefined = Enum.IsDefined(category1); // true
PlanetCategory category2 = (PlanetCategory)99999;
bool category2IsDefined = Enum.IsDefined(category2); // false
PlanetFeatures features1 = PlanetFeatures.HasMoons;
bool features1IsDefined = Enum.IsDefined(features1); // true
PlanetFeatures features2 = PlanetFeatures.HasMoons | PlanetFeatures.HasRings;
bool features2IsDefined = Enum.IsDefined(features2); // false
PlanetFeatures features3 = (PlanetFeatures)99999;
bool features3IsDefined = Enum.IsDefined(features3); // false
Conclusion
Enums are a nice tool to use. Some details can lead to unexpected behavior but are easy to solve. In the upcoming .net 8 release, the Enums will get even faster in the runtime which is a nice addition.
You can find a working code example in my GitHub repository.