Introduction
In this post, we’re going to look at the enum
value types, how they can be composed as flags and how C#7 makes it easier for us to use and understand.
We touched upon enum
s during the post "The hidden side-effect of enums and values." Now we’re going to have a more in-depth look at them, see what they are and how they work, and we’re going to expand on those ideas.
What are Enums?
Enum
s are short for enumerations, they represent a set of constants mapped out to a numerical value. This allows us to make use of those values without using “magic” numbers or parsing string
s.
Let’s look at an example of an enum
and dissect it:
public enum ModeOfTransport : byte
{
ByCar = 5,
ByBicycle,
ByAirplane,
ByDingy = 10,
ByCanoe = 10
}
Let’s assume that this enum
will be used in a map application to tell us the route to take and how long until we reach the destination.
In this example, we can see the following:
- An
enum
represents a number, as such, when we derive it from a numerical value like byte
, we are saying that this enum
cannot have negative values, and cannot take up more than 256 values (if we try to assign negative numbers or numbers higher than 255, the compiler will issue an error). Do note that if no numerical type is present, then it will default to a 32-bit integer, which for small enum
s can be a big waste if cumulated. - We can assign numerical values to each
enum
so we can have better control over which number represents which value. Enum
s, by default, start counting from 0
even if the underlying type is a signed integer, and they keep counting up from where they were last off so ByBicyle
will have a value of 6
, and if we were to add another enum
definition after ByCanoe
, it will have a value of 11
. - In this case, we opted to start from
5
with the first value, but we can also assign the same numerical value to different enum
definitions like in the case of a dingy and a canoe. The reasons for using the same numerical value are not encountered often but they do occur, like, for example, someone during the development of the application created a client application that uses both dingy and canoes as means of transportation, but for our algorithm since they are both row-boat, we want to treat them the same. Enum
s are easier to use with switch
cases because during compile time, they are turned into their numerical value, word of caution if we have the same numerical values in the same enum
, and we put them in a switch
case, the compiler won’t allow us to do that since the value is already registered as a case. Here’s an example:
switch (enumVal)
{
case ModeOfTransport.ByCar:
break;
case ModeOfTransport.ByBicycle:
break;
case ModeOfTransport.ByAirplane:
break;
case ModeOfTransport.ByDingy:
break;
case ModeOfTransport.ByCanoe:
break;
default:
throw new ArgumentOutOfRangeException();
}
This will emit an error saying the following:
The switch
statement contains multiple cases with the label value ’10
’.
Duplicate case label value ‘ModeOfTransport.ByDingy
’
What Are Flags and What Do Binary Literals Have to Do With It?
Since enum
s are a representation of a number, before we can look at flags properly, we have to understand how they are represented in memory and how they can become “composable”.
Take for example, our ModeOfTransport enum
, since it “inherits” from a byte
that means that the value ranges from 0
to 255
inclusive. So the memory values it can have are from 00
to FF
in hexadecimal or from 0000 0000
to 1111 1111
in binary, we will see why the binary form is so important.
If we look at our values now in the binary form, we will see the following:
ByCar = 0000 0101,
ByBicycle = 0000 0110,
ByAirplane = 0000 0111,
ByDingy = 0000 1010,
ByCannoe = 0000 1010
For me, this makes more sense when I look at them like this in binary because it means that every bit in that enum
represents something for us. This makes for compact reusable dictionaries, and since enum
s are value types, it means they always have to have a value and are always copied, from method calls to large arrays.
Now let’s look at the Flags
attribute when declaring an enum
. For this example, consider we want to make a game in which we want to tell the player in which directions he or she can move:
[Flags]
public enum Directions : byte
{
North,
East,
South,
West
}
Let’s look at what is wrong in this enum
and the several ways we can fix it.
If the player was in a room that had all 4 paths open, then using the current enum
, only the West
path will show up. Flags
are a special type of enum
which can be composed of multiple values, hence by convention they are named in a plural form. To understand flags, we must look into binary operations and how that allows us to make one number represent several things at the same time.
In our case, the values are as follows:
Value | Decimal | Hexadecimal | Binary |
North | 0 | 0x00 | 0000 0000 |
East | 1 | 0x01 | 0000 0001 |
South | 2 | 0x02 | 0000 0010 |
West | 3 | 0x03 | 0000 0011 |
When we compose enum
s (and any number in general), we use the bitwise operators. Those are & (AND), | (OR), ^ (XOR), (SHIFT RIGHT). Let’s have a quick recap as to how they work and then the problem with the enum
above will become obvious.
Operation | Operands | Result | Explanation |
& (AND) | 0000 0001 | 0000 0001 | When we use binary AND , we compare each bit and if both are 1 , then the resulting value is 1 , else the value is 0 |
0000 0011 |
| (OR) | 0000 0001 | 0000 0011 | When we use binary OR , we compare each bit and if either bit is 1 , then the resulting value is 1 , else the value is 0 |
0000 0010 |
^ (XOR or exclusive OR) | 0000 0001 | 0000 0010 | When we use binary exclusive OR , we compare each bit and only if one bit is 1 and the other is 0 , then the resulting value is 1 , else the value is 0 |
0000 0011 |
~ (NOT) | 0000 0001 | 1111 1110 | When we use binary NOT , we flip the value of each bit, 1 becomes 0 and 0 becomes 1 |
Unary operation, no second operand |
<< (SHIFT LEFT) | 1100 0001 | 1000 0010 | When we use binary SHIFT LEFT , all the bits get shifted to the left times the number of the second operand, if we shift past the end, then that bit is lost |
1 |
>> (SHIFT RIGHT) | 0000 0011 | 0000 0001 | When we use binary SHIFT RIGHT , all the bits get shifted to the right times the number of the second operand, if we shift past the end, then that bit is lost |
1 |
You can find more information about binary operator here.
Now let’s look at our problem, if we wanted to say the player can go North
, West
or South
, then we would write:
Directions.North | Directions.South | Directions.West
Which would get translated into binary as such:
0000 0000 |
0000 0010 |
0000 0011 =
0000 0011
Because we’re comparing each bit by column, we will end up with the value for West
. This is not what we wanted, from 3 choices, we’re down to 1.
To solve this, each enum
value must represent a single independent bit, as such the values for our directions should be as follows:
Value | Decimal | Hexadecimal | Binary |
North | 1 | 0x01 | 0000 0001 |
East | 2 | 0x02 | 0000 0010 |
South | 4 | 0x04 | 0000 0100 |
West | 8 | 0x08 | 0000 1000 |
Notice the common theme, we move one bit to the left and the decimal value gets multiplied by 2
. Using these values, we can see that the earlier operation turns into:
0000 0001 |
0000 0100 |
0000 1000 =
0000 1011
Now we have no overlap and our algorithms will work properly because instead of each bit meaning something like in the case of simple enum
s, we only look at bits at certain positions, that means that for a byte enum
, we might have 8 possible base values but 255 combinations of them.
And now, let’s see how we can do this in code in each of the 4 forms, first up decimal:
[Flags]
public enum Directions : byte
{
North = 1,
East = 2,
South = 4,
West = 8
}
While we might be more familiar with the decimal system, this method of writing it means that we must always hard code the value with a calculation we must do, can anyone tell me what is the value of 2 raised to the power of 17?
Now for the hexadecimal form (or hexadecimal literals):
[Flags]
public enum Directions : byte
{
North = 0x1,
East = 0x2,
South = 0x4,
West = 0x8
}
Not a lot of change in this form because we have such a small number of values, but the advantage of this approach is that you can write very large numbers in a small screen space and they are easy to turn back into base 2 (though not off the top of the head).
[Flags]
public enum Directions : byte
{
North = 1 << 0,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3
}
This one is my second favorite approach, the down side is that you need to understand how bit shifting works, the advantage is that you can add any value without doing any computation, for example, here I know that West
is 1 shifted 3 times, which means it’s the 4th bit.
And now the latest mode of writing comes from the release of C# 7 under the name of binary literals.
[Flags]
public enum Directions : byte
{
North = 0b0000_0001,
East = 0b0000_0010,
South = 0b0000_0100,
West = 0b0000_1000
}
This has become my new favorite way of writing enum
flags, the downside is obvious, it is very verbose, but the advantages for me are obvious, I can see each bit in each position and I don’t need to calculate them. Do note that the front zeros are not mandatory but it’s a good habit to write them like this because it keeps everything in line and also this way, we can visualize the maximum and minimum value for my underlying numeric type.
Conclusion
I hope you enjoyed our little trip through the world of bits and I hope this will be of help to you in the future, as it has been for me.
Thank you and see you next time.
CodeProject