C# .NET Enum, Constant and Static ReadOnly
Enums Can Behave Like Constants
The C# .NET language has constants, and it has variables. A constant is essentially a value that is known at compile-time, whereas a variable is essentially a placeholder of a specific type for a value that can change at runtime. An enum
is a bit of an anomaly because it defines a type with unique set of elements backed by an integer type (which is Int32
by default, but can be changed). So, the question becomes
Do the elements of the enum behave like constants because they are backed by integers, or like variables because an enum variable is created from the defined enum type?
If you think about it, you probably already know the answer. Since you can switch
on a variable that is an enum
using the enum's elements as case
conditions, the enum elements must behave like constants because C# only allows constants, literals and enums (and expressions that can get resolved to a fixed value at compile-time) to be used as case
conditions!
public MyEnum
{
Alpha,
Bravo
}
MyEnum myEnum;
switch (myEnum)
{
case MyEnum.Alpha:
...
break;
case MyEnum.Bravo:
...
break;
}
But, that is not the end of the story. After all, there is a entire type backing each enum
variable, so an enum
must be more than a simple collection of constants. And it is.
First a little roadtrip...
Many C# programmers are already familiar with the pitfalls of using constants. Consider the following example.
// Defined in Assembly_A
public static class MyValues
{
public const int Alpha = 1;
public static readonly int Bravo = 2;
}
...
// Defined in Assembly_A
public void MyMethodA()
{
SomeMethodToCall(
MyValues.Alpha,
MyValues.Bravo);
}
...
// Defined in Assembly_B
public void MyMethodB()
{
SomeMethodToCall(
MyValues.Alpha,
MyValues.Bravo);
}
Because a constant is a value that is known at compile-time, the compiler inserts the actual value directly into the code that uses the constant. Likewise, because a variable is a placeholder for a value that can change at runtime, the compiler inserts a link to the variable into the code that uses the variable.
If the code that uses the constant is within the same assembly in which the constant is defined, then there is no risk of the values getting out of sync. But, if the code that uses a constant is within a different assembly than the code that defines the constant, the values can get out of sync if the assembly containing the constant is recompiled but the other assembly that references the constant does not get recompiled.
In the example above, MyMethodA()
is defined in the same assembly as MyValues
. Therefore, MyMethodA()
always uses the correct values for both MyValues.Alpha
and MyValues.Bravo
.
But, MyMethodB()
is defined in a different assembly than MyValues
. Therefore, it is possible for MyValues.Alpha
in MyMethodB()
to get out of sync and use the old value of MyValues.Alpha
should it's value change and Assembly_A gets recompiled but Assembly_B does not get recompiled using the latest Assembly_A. Interestingly, MyValues.Bravo
cannot get out of sync because it is a static readonly
instead of a const
. Therefore, MyValues.Bravo
is a true variable that is set at runtime, but it just so happens that the value is set exactly once when the MyValues
type is loaded and cannot be changed after that so it is essentially a "runtime constant" instead of a "compile-time constant".
Returning home to enums...
When code uses an enum element, such as MyEnum.Alpha
, the compiler compiles-in the actual, underlying integer value just like it does for a constant. That is why enum elements can be used as case
conditions. But, that also means enums can have the same pitfalls as constants: if an enum
is defined in one assembly, and elements are added or removed, or the underling values are otherwise changed, then the code use those enum elements in another assembly can get out of sync if the assembly containing the enum type definition is recompiled but the assembly using an enum element is not recompiled.
But, recall that an enum has an entire custom type behind it. And, that type allows the enum values to easily be changed between the enum element name, underlying integer value, and string representation of the element name.
// Convert enum element to integer: myEnumAsInt == 0
int myEnumAsInt = (int)MyEnum.Alpha;
// Convert enum element to string: myEnumAsString == "Alpha"
int myEnumAsString = MyEnum.Alpha.ToString()
// Convert integer to enum element: myEnumFromInt == MyEnum.Alpha
MyEnum myEnumFromInt = (MyEnum)0;
// Convert string to enum element: myEnumFromString == MyEnum.Alpha
MyEnum myEnumFromString = (MyEnum)Enum.Parse(typeof(MyEnum), "Alpha");
// Try convert string to enum element:myEnumFromString2 == MyEnum.Bravo
MyEnum myEnumFromString2;
Enum.TryParse("Bravo", true, out myEnumFromString2);
Where this gets really interesting is when all of the following are true at the same time
- The enum type is defined in one assembly but its elements are used in another assembly
- The assembly containing the enum type is recompiled, but the assembly using the enum elements is not recompiled to use that new assembly
- The enums elements are converted to and or from strings
As an example, let's say Assembly1 defines MyEnum:
using System;
namespace LibraryThatDefinesMyEnum
{
public enum MyEnum
{
Alpha,
Zulu
}
}
And, let's say Assembly2 uses MyEnum elements from Assembly 1:
using LibraryThatDefinesMyEnum;
using System;
namespace AppThatUsesMyEnum
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(
"Enum Value Names = "
+ String.Join(", ", Enum.GetNames(typeof(MyEnum))));
WriteMyEnumValue(MyEnum.Alpha);
WriteMyEnumValue(MyEnum.Zulu);
WriteMyEnumValue("Alpha");
WriteMyEnumValue("Zulu");
}
private static void WriteMyEnumValue(
MyEnum myEnum)
{
Console.WriteLine(string.Format(
"'{0}' -> {1}",
myEnum.ToString(),
(int)myEnum));
}
private static void WriteMyEnumValue(
string myEnumAsString)
{
MyEnum myEnumm = (MyEnum)Enum.Parse(typeof(MyEnum), myEnumAsString);
WriteMyEnumValue(myEnumm);
}
}
}
If Assembly1 is compiled first, and then Assembly2 is compiled to use Assembly1, the results are as expected:
Output 1:
Enum Value Names = Alpha, Zulu
'Alpha' -> 0
'Zulu' -> 1
'Alpha' -> 0
'Zulu' -> 1
Now, let's say that MyEnum is tweaked to add new element, Bravo. And, Assembly1 is recompiled but Assembly2 is not recompiled. Now the results are out of sync:
using System;
namespace LibraryThatDefinesMyEnum
{
public enum MyEnum
{
Alpha,
Bravo,
Zulu
}
}
Output 2:
Enum Value Names = Alpha, Bravo, Zulu
'Alpha' -> 0
'Bravo' -> 1
'Alpha' -> 0
'Zulu' -> 2
What the heck?
- The "Bravo" started showing up on the first line
- The first
'Zulu' -> 1
line just changed to'Bravo' -> 1
- The second
'Zulu' -> 1
line just changed to'Zulu' -> 2
Now, if Assembly2 is recompiled so that it picks up the changes from Assembly1, the everything is back in sync once again:
Output 3:
Enum Value Names = Alpha, Bravo, Zulu
'Alpha' -> 0
'Zulu' -> 2
'Alpha' -> 0
'Zulu' -> 2
So what exactly happened when everything went out of sync in the second output?
- Assembly1 redefined the MyEnum type, and that new type definition was available to Assembly2 even though Assembly2 was not recompiled.
- When Assembly2 calls
num.GetNames()
it does so on the type, which is the new type, so the "Bravo" showed up. - The Alpha element did not get its numeric value changed because Bravo element was added after Alpha. But, the Zulu element did get its numeric value changed because the Bravo element was inserted before Zulu.
- When the compiler created Assembly2 the first time (first and second outputs), it compiled
WriteEnumValue(MyEnum.Zulu)
intoWriteEnumValue(1)
. But, when the compiler ran the second time (output 3) it then compiledWriteEnumValue(MyEnum.Zulu)
intoWriteEnumValue(2)
. - The ToString() method is called on the type which lead to
'Bravo' -> 1
being displayed forWriteMyEnumValue(MyEnum.Zulu);
in output 2 because the Bravo element was associated with the value 1 in the type definition. - The Enum.Parse() method is called on the type which lead to
'Zulu' -> 2
being displayed forWriteMyEnumValue("Zulu");
because the Zulu element was associated with the value 2 in the type definition.
So, as you can see, when the enum elements are used directly (in assigments, passed into functions, in conditionals, in switch case, etc.) the numeric values are compiled in like const
, and so do not change when the type definition has changed until Assembly2 is recompiled. But, when the methods from the type are used they are referring to the current type definition.
Programmer, Engineer