Let's put that superclass through its paces with a few NUnit tests.
First off, EnumBase<T> provides a Name() method that returns a string version of the
element name:
[TestFixture]
class EnumTests
{
[Test] public void TestName()
{ Assert.AreEqual("FIRST", TestEnum.FIRST.Name() );
Assert.AreEqual("SECOND", TestEnum.SECOND.Name() );
}
A ToString() does the same thing, but ToString() might be overridden in the
subclass and Name cannot be overridden, so it's more reliable:
[Test] public void TestToString()
{ Assert.AreEqual("FIRST", TestEnum.FIRST.ToString());
Assert.AreEqual("SECOND", TestEnum.SECOND.ToString());
}
The Enum<T> also provides a way to convert a string to an enum element, which is convenient when
you're doing things like reading configuration files that use enum values. This
conversion is done with a static ValueOf(string) method:
[Test] public void TestValueOf()
{ Assert.AreEqual(TestEnum.FIRST, TestEnum.ValueOf("FIRST"));
Assert.AreEqual(TestEnum.SECOND, TestEnum.ValueOf("SECOND"));
// test that an exception is thrown if the argument isn't an enum element:
Assert.Throws<ArgumentException>( ()=>TestEnum.ValueOf("Can't Exist") );
}
We sometimes need to be able to use the enum as an array index,
so an Ordinal() method (that returns
a unique integer value for each enum element) is provided for that purpose.
Ordinal values start at 0, and are assigned in declaration order:
[Test]
public void TestOrdinals()
{ Assert.AreEqual( 0, TestEnum.FieldOne.Ordinal() );
Assert.AreEqual( 1, TestEnum.FiledTwo.Ordinal() );
}
Finally, we need to be able to enumerate across the enum elements.
TestEnum.Values() returns an IEnumerable collection of the elements,
so a foreach statement works as expected:
[Test] public void TestValues()
{ ICollection<TestEnum> values = new List<TestEnum>();
foreach( var element in TestEnum.Values() )
values.Add(element);
Assert.AreEqual(2, values.Count);
Assert.IsTrue(values.Contains(TestEnum.FIRST));
Assert.IsTrue(values.Contains(TestEnum.SECOND));
}
}
}
The only thing that's missing is a way to do a switch. The Ordinal()
method isn't useful for that because the case values have to be constants, so it's not really
a solvable problem.
In a pinch, you can simulate a switch with a sequence of if/else statements that don't have
that restriction.
Implementing EnumBase<T>
The real work is all in the EnumBase<T> class, so let's look at that (This class is in the same
listings file as the eariler NUnit tests). Starting at the top with a few field declarations:
using System;
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Collections.Generic;
namespace Tools
{
public class EnumBase<T> where T:EnumBase<T>
{
private string fieldName; // = null;
private int myOrdinal = -1;
private static readonly Lazy<IReadOnlyDictionary<string, T>> enumElements = // don't change this name!
new Lazy<IReadOnlyDictionary<string, T>>
( ()=>
{
IDictionary<string, T> dict = new Dictionary<string, T>();
FieldInfo[] theFields =
typeof(T).GetFields( BindingFlags.Static
| BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach( var item in theFields )
{
if( item.IsInitOnly ) // it's readonly. Alread checked for static public with BindingFlags
{
var theValue = (T) item.GetValue(null);
if (theValue.GetType() == typeof (T)) // want an exact match. Don't use "as" or "is"
dict.Add(item.Name, theValue);
}
}
return new ReadOnlyDictionary<string, T>(dict);
}
);
The field name and the ordinal are self-explanatory.
Bear in mind that this class defines the fields and methods of each enum element, so every element carries
around it's own field name and ordinal value.
We'll look at how they're initialized shortly.
The enumElements field requires quite a bit more explanation. First, I'm using the
IReadOnlyDictionary interface and
ReadOnlyDictionary class. These are new to .NET, and aren't available if you're using Visual Studio 10.
I've provided versions in the listings file in case you need them (simple wrappers around Dictionary that
throw exceptions if you try to modify the contents).
The main issue is the use of a Lazy<T>.
This class just wraps a reference to another class. It calls the lambda that's passed in as an argument
the first time you access enumElments.Value, returning a reference to the encapsulated object.
Think of the lambda as a kind of factory method: The factory is invoked at first access to create
the object. The factory method is fully thread safe, so you don't have to worry about two threads
accessing Value simultaneously.
The enumElements dictionary holds all the enum elements and is indexed by the name of the element represented as a string.
The problem that the Lazy<T> is solving is order of initialization. enumElements is a superclass field, but it holds
a list of objects that are created by the subclass. C# does define an order of initialization, but it turns out that this order
is not always honored by debuggers. The best solution is to hold off on initializing the list until after all construction
(both super- and subclass, both static and instance) is completed. The object must be completely initialized, of course,
when we call a method that accesses enumElements, so that's a safe time to initialize.
Looking at the actual initialization code, I'm getting the names of the enum elements out of the subclass using the
magic of introspection. The code is simplified, considerably, by the generic argument T, which represents the subclass.
Taking IDictionary<string, T> dict = new Dictionary<string, T>(); as an example. Given
class MyEnum : EnumBase<MyEnum>
{
public static readonly MyEnum MEMBER = new MyEnum();
//...
}
T will represent the class MyEnum, so the dictionary is effectively a Dictionary<string,MyEnum>.
Without using generics, the superclass's work would be much more difficult.



