Channels ▼
RSS

C/C++

Fixing the enum in C#


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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video