To me, enums in the .NET Framework 2.0 don’t feel like a polished feature. Writing
serious code with them is a descent into typecasting madness filled with the maniacal
laughter of bitwise ands and ors. Maybe the problem is that they are considered
"good enough" or simply "not interesting enough" to improve. I mean, what’s more
exciting to a compiler writer: enumerations or generics and lambda expressions?
It’s possible that enums haven’t been improved simply because of time constraints
or resources. But, in my humble opinion, it’s high time they deserved some love.
In this article, I offer up my list of potential improvements for .NET enums.
When possible, I present workarounds to implement the improvements with the current
or next version of the .NET framework.
Implicit Conversion to Underlying Type
Every enum has an underlying type that is one of the following: System.Byte,
System.Int16, System.Int32, System.Int64, System.SByte, System.UInt16, System.UInt32
or System.UInt64. This underlying type is used to store the actual numeric value
of the enum. By default, the underlying type is System.Int32 but a different underlying
type can be chosen when defining an enum:
enum Fruit: byte {
Apple, Banana, Orange }
If I want to convert a value from the above enum, I have to typecast:
byte theFruit = (byte)Fruit.Banana;
Because Fruit is explicitly declared to have an underlying type of System.Byte,
it seems redundant to the extreme to require a typecast to a variable of that same
type. Especially, when implicit conversions are available in other situations where
a numeric value is guaranteed to fit inside another with no loss of precision:
byte b = 42;
int i = b;
I propose that enums are, at the very least, made implicitly convertible to their
underlying type. When converting from the underlying type to the enum, it seems
good practice and protective to require a typecast in case the enum doesn’t define
the appropriate named value.
byte theFruit = Fruit.Banana;
Fruit otherFruit = (Fruit)4;
I’m sure that the type safety police will be out in force on me for this but
this seems dogmatic just for the sake of being dogmatic. Some might argue that a
typecast improves readability in the above code because the type is visible on both
sides of the assignment but I would argue that the typecast actually adds visual
noise and makes it harder to read.
Generic Constraint
Currently, there is no way to constrain a generic type parameter to an enum.
In fact, it is illegal to constrain generic type parameter to inherit from System.Enum.
So, neither of these method declarations is legal:
static class EnumHelpers
{
public static T[] GetValues<T>()
where T: enum;
public static T[] GetValues<T>()
where T: System.Enum;
}
We can constrain the generic type parameter to a struct since all enums implicitly
inherit from System.ValueType but runtime code must be written to further constrain
it to an enum:
static class EnumHelpers
{
public static T[] GetValues<T>()
where T: struct
{
if (!typeof(T).IsEnum)
throw new
ArgumentException("Type argument 'T'
must be an enum.");
return (T[])Enum.GetValues(typeof(T));
}
}
The above code works fine except that it fails to generate a compile time error
because any struct can specified for T. So, this method will compile but fail spectacularly
if called like this:
int[] values = EnumHelpers.GetValues<int>();
In my opinion, the biggest benefit we would get from a new enum constraint for
generics is the possibility of a long overdue update to the System.Enum class. Currently,
every public static method on System.Enum declares its first parameter as a System.Type.
Each method has checks on this parameter to determine whether it is an enum and
throw the appropriate exception if it isn’t. In addition, any method that returns
an enum value or takes an enum value as a parameter uses System.Object. This causes
unnecessary boxing and unboxing operations which seem really wasteful considering
that enums are supposed to be lightweight.
Generic versions of the public static methods on System.Enum with an enum constraint
could remove the System.Type parameter and the runtime checks associated with it.
This allows client code to remove cumbersome typeof() operators. Also, because no
exceptions would be thrown, try/catch handlers could be removed from code that calls
these methods. In addition, the boxing and unboxing operations would be removed
as well as any previously required typecasts in calling code. In essence, there
would be benefits in both performance and code readability.
Today, we can achieve improved code readability by creating our own class that
wraps the System.Enum static methods with generic methods. Such a class actually
reduces performance slightly because it is another layer of indirection between
the client code and System.Enum. But, 99% of the time, readability would be preferred
over this minor performance hit.
Compiler-Generated Static Methods
In addition to updating the static methods on System.Enum, it would be useful
for the compiler to generate the necessary public static methods on the enum itself.
Compiler-generated methods aren’t a new concept to the .NET Framework. For example,
BeginInvoke, EndInvoke and Invoke methods are generated for delegates. I propose
generating all of the static methods that appear on System.Enum onto all new enums
at compile time. With this in place, code could be written like this:
using System;
static class Program
{
enum Day { Sunday, Monday, Tuesday, Wednesday,
Thursday, Friday, Saturday };
static Main(string[]
args)
{
Array.ForEach(Day.GetNames(),
Console.WriteLine);
}
}
With compiler-generated static methods on enums, users could forget about System.Enum
completely.
Please note that any enum inherits the static methods of System.Enum (even though
they don’t appear in Intellisense). So, the above code could be rewritten like this:
using System;
static class Program
{
enum Day { Sunday, Monday, Tuesday, Wednesday,
Thursday, Friday, Saturday };
static Main(string[]
args)
{
Array.ForEach(Day.GetNames(typeof(Day)),
Console.WriteLine);
}
}
However, passing “typeof(Day)” into a method on the “Day” type seems pretty redundant
to me.
Instance Methods
Giving developers the ability to declare instance methods on enums allow any
utility functions that operate on a particular enum to be place on the enum itself.
Here’s what I have to write today:
using System;
static class Program
{
enum Day { Sunday, Monday, Tuesday, Wednesday,
Thursday, Friday, Saturday };
static bool IsWeekday(Day day)
{
return (day !=
Day.Sunday && day !=
Day.Saturday);
}
static void Main(string[]
args)
{
foreach (Day day
in Enum.GetValues(typeof(Day)))
Console.WriteLine("{0,10} = {1}", day, IsWeekday(day));
}
}
Here’s what I would like to be able to write:
using System;
static class Program
{
enum Day
{
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday;
public bool IsWeekday()
{
return (this !=
Day.Sunday
&& this != Day.Saturday);
}
};
static void Main(string[] args)
{
foreach (Day day
in Enum.GetValues(typeof(Day)))
Console.WriteLine("{0,10} = {1}", day, day.IsWeekday());
}
}
Fortunately, there is a workaround if I have the LINQ preview installed. With
a little extra code, I can simulate instance methods using C# 3.0’s extension method
feature:
using System;
static class Program
{
enum Day { Sunday, Monday, Tuesday, Wednesday,
Thursday, Friday, Saturday };
static class DayMethods
{
public static bool IsWeekday(this
Day day)
{
return (day !=
Day.Sunday && day !=
Day.Saturday);
}
};
static void Main(string[]
args)
{
foreach (Day day
in Enum.GetValues(typeof(Day)))
Console.WriteLine("{0,10} = {1}", day, day.IsWeekday());
}
}
Using an extension method, I can achieve exactly the same client code but I have
to do a little bit more coding and create a new class to hold the method. It seems
to me that the C# team could add instance methods to enums for C# 3.0 by generating
extension methods at compile time.
Where Are We?
To many, enums are perfectly satisfactory as they are. There really is very little
that can't do with an enum today. But, if you want to code them with elegance and
style, they'll need a bit of love first.