C# Enum Craziness: Sometimes What You Expect Isn't The Case
I learned something new today about enums that I find really weird. Lets start with the following test enum:
public enum Action
{
Run = 2,
Walk = 4,
Crawl = 8
}
and then some code to do something with that enum:
static void Main(string[] args)
{
Console.WriteLine((Action)2);
Console.WriteLine((Action)4);
Console.WriteLine((Action)8);
Console.WriteLine((Action)10);
}
What do you think will happen here? Will it even compile?
When I saw this code snippet I said to myself, "the first three lines look ok but the last line won't work because 10 isn't a valid value for this enum. Well, I was wrong. This program actually outputs:
Run
Walk
Crawl
10
Huh?!? How could this be? 10 isn't a valid value according to my enum definition!
This Should Never Happen...Right?
Ok, lets try something different. How about a method?
public static void Execute(Action action)
{
switch (action)
{
case Action.Run:
Console.WriteLine("Running");
break;
case Action.Walk:
Console.WriteLine("Walking");
break;
case Action.Crawl:
Console.WriteLine("Crawling");
break;
default:
Console.WriteLine("This will never happen! {0}", action);
break;
}
}
Surely you can't pass anything into this method other than one of the 3 values defined in my enum. So lets run some code:
static void Main(string[] args)
{
Execute(Action.Run);
Execute(Action.Walk);
Execute((Action)55);
}
What do you think this does? Well, it outputs:
Running
Walking
This will never happen! 55
Just about this time you must be thinking: "This has to be a bug!". Well, it is not. It is by design. here is the excerpt from the C# design spec:
14.5 Enum values and operations Each enum type defines a distinct type; an explicit enumeration conversion (Section 6.2.2) is required to convert between an enum type and an integral type, or between two enum types. The set of values that an enum type can take on is not limited by its enum members. In particular, any value of the underlying type of an enum can be cast to the enum type, and is a distinct valid value of that enum type.
Wow, that is not at all what I expected when it comes to limiting the possible values of enums. So what are enums good for then? Are they just for code readability? Between this little revelation and my previous hack to associate string values to enums, I am loosing faith in enums.
Can You Handle it?
So what is the best way to check for this in your methods? Should you throw an exception if you receive an enum value you were not expecting? Should you just ignore it? Well, lets see what the .Net base class libraries do.
First, lets start with the System.IO.File class. What happens if I run the following code?
File.Open(@"C:\temp\test.txt", (FileMode)500);
Well, it throws a System.ArgumentOutOfRangeException with the message "Enum value was out of legal range.". Ok, makes sense - I did pass in something out of range.
Lets try reflection. What does the following code snippet output?
PropertyInfo[] info = typeof(StringBuilder).GetProperties((BindingFlags)303);
Console.WriteLine(info.Length);
Well, it just outputs '0' which means in this case it is just being ignored and an empty array is returned.
What about System.String? Lets try this code snippet:
"TestString".Equals("TESTSTRING", (StringComparison)245);
Well, it turns out this throws an exception but instead of being a System.ArgumentOutOfRangeException like System.IO did, it throws a System.ArgumentException with the message "The string comparison type passed in is currently not supported." Ok, so this is kind of the same but still a little inconsistent if you ask me.
Is Anything Safe These Days?
So what is a developer to do? Obviously you need to be aware of this when you are receiving enum types from publicly facing code. It seems there is no clear guidance on this that I can find. The C# design spec explains the behavior but doesn't really give any guidance on why or how this should be handled. So the only other place to turn for guidance is one of my favorite .Net books Framework Design Guidelines by Brad Abrams and Krzysztof Cwalina. On their section on enums, I can't find any guidance on how to handle out of range enum values. I do, however, find guidance that we should be using enums:
DO use an enum to strongly type parameters, properties, and return values that represent sets of values
They also suggest that enums should be favored over static constants:
DO favor using an enum over static constants
And Jeffrey Richter (and while I am pointing out my favorite .Net books, I have to add Mr. Richter's book CLR via C# which contains priceless information on the CLR that you can't find anywhere else) adds the following commentary:
An enum is a structure with a set of static constants. The reason to follow this guideline is because you will get some additional compiler and reflection support if you define an enum versus manually defining a structure with static constants.
So I guess we do continue to use enums and just know that we can't always trust their values to be valid. Do you think the C# design spec should be amended to include a recommended behavior for out of range enum values? Perhaps Brad and Krzysztof can include something in their second edition of Framework Design Guidelines.