I really like some features of C# (lambdas and delegates, out
parameters, C#'s variant on generics).
and I really dislike some other features (such as properties or explicit initializations that circumvent the constructor).
But some parts of the language are just plain disappointing. The enum
is the feature at the front of that parade. I'll explain the problem
shortly and show how it can be fixed with a little code. Along the way, I'll also look at a reasonable application of the reflection APIs.
The Problem With C# enum
s
The difficulties with the C# enum
stem primarily from its history.
The C# enum
is pretty-much identical to the C++ enum
,
which was, in turn, based on standard C.
C used used a simple integer to represent an enum
element, and so does C#.
The problem with this integer implementation is that it's too easy to spoof. Any integer can, ultimately
be an enum
, even integers that aren't legal values. Consider the following code (which
compiles just fine resharper doesn't even complain about it):
class foo</a> { public enum Suite { Hearts, Clubs, Diamonds, Spades }; public void dealer() { f( (Suite) 10 ); } public void f( Suite theSuite ) { switch (theSuite) { case Suite.Hearts: /*...*/ break; case Suite.Clubs: /*...*/ break; case Suite.Diamonds: /*...*/ break; case Suite.Spades: /*...*/ break; } } }
The f()
method fails miserably because 10
doesn't represent any of the actual values
assigned to the enum
elements, so none of the cases will run.
Were an enum
implemented correctly by the language, the foregoing code wouldn't
compile at all. It just shouldn't be possible for theSuite
to not represent a legal Suite
.
The other problem with C#'s enum
mechanism is that it isn't a real class, as it is in
Java. A C# enum
can't have methods, for example,
or implement interfaces. As we've seen in my enum
-related articles over the past few months,
those facilities are pretty useful. For example, in Java, you can write an enum
that verifies
that all its members exist in a database table that defines the same enum
at the database level (and
vice versa). Because the code verifies itself, you never need to worry about the code and
database getting out of synch with each other. However, you need to define methods in the enum
definition
to pull this feat off.
Fortunately, we can fix all this.
Back before Java had enum
s, programmers implemented them using classes. (In fact, Java enum
s
are still effectively implemented that way under the covers.)
A Java enum
for Suite
is implemented by the compiler more or less like this:
class Suite extends Enum { public static final Suite HEARTS = new Suite(); public static final Suite CLUBS = new Suite(); public static final Suite DIAMONDS = new Suite(); public static final Suite SPADES = new Suite(); private Suite() }
Of course, you can do exactly the same thing in C# (with minor tweaks to the syntax and naming conventions):
public class Suite : EnumBase<Suite> // I'll explain this superclass shortly { public static readonly Suite Hearts = new Suite(); public static readonly Suite Clubs = new Suite(); public static readonly Suite Diamonds = new Suite(); public static readonly Suite Spades = new Suite(); private Suite() { } }
The private constructor assures that the only four instances of Suite
that can possibly exist are those that are allocated within the class itself (Suite.HEARTS
, Suit.CLUBS
, etc.). That is, the sort of spoofing I discussed earlier simply isn't possible. If you pass a Suite
reference to a method, that reference cannot possibly
refer to anything other than a legal Suite
element (or null
, of course).
You can also compare using ==
without difficulty:
void f( Suite someSuite ) { if( someSuite == Suite.HEARTS ) //... }
A roll-your-own enum
can also implement interfaces, define methods (and constructors which are
used to create the individual enum
elements), and so forth.
You may want to review my other enum
-related articles to see why that's such a good thing.
The only real (and unsolvable) issue is that you can't switch
on a Suite
. However,
you can accomplish the same thing with a series of nested if
/else
statements.
For what it's worth, the presence of a switch
is considered a "bad smell"
in OO systems it's usually an indication that you're not using interfaces as much as you should.
The nullability of a class-style enum
is also a mixed blessing, by the way. Because it is a
value object, a C# enum
can't be null
.
Consequently, a method that takes a standard
enum
parameter is assured that there will be something there (even
if that something has a nonsensical value). A class-style enum
can be
null
, however, so it's best to put a
if(myEnumArgument == null) throw new ArgumentNullException("myEnumArgument");
or equivalent at the top of every method that takes a class-style-enum
argument.
An if
statement is probably a better choice than a Debug.Assert(...)
here.
Using the EnumBase
Class to Make an enum
The only problem with the class-based-enum
approach is the absence of the useful features that are provided
by the language. For example, you can't iterate across the enum
elements, or get a string
that holds the enum
name. Most of that work is done in Java and C#'s Enum
superclass, but
the C# Enum
superclass isn't much use to us if we're implementing the enum
pattern from
scratch, as it depends on compiler-generated magic.
This problem is solvable by writing our own EnumBase<T>
superclass.
Let's start by looking at how the superclass used. (The listings archive contains all the code I'm about
to discuss.)
A few unit tests demonstrate the facilities supported by the superclass. First, I define a test enum
:
using System; using System.Collections.Generic; using NUnit.Framework; using Tools; // the EnumBase<T> is defined in this namespace namespace EnumTest { public class TestEnum : EnumBase<TestEnum> { public static readonly TestEnum FIRST = new TestEnum(); public static readonly TestEnum SECOND = new TestEnum(); private TestEnum() { } }
The main thing to notice is the EnumBase<TestEnum>
superclass. Admittedly, it is odd to
pass the current class name into the generic argument of a superclass, but it's perfectly
legal. I'll explain how to use that class information in a moment, but for now, look at this
parameter as a convenient way to pass a subclass name to the base class.