Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Enum Generitized

Rate me:
Please Sign up or sign in to vote.
3.28/5 (10 votes)
12 May 2007CPOL7 min read 34.9K   164   26   11
Using Generics to make a different kind of enumeration: easy to comment, and supports many types.

Introduction

In Enum Redo, we saw how an abstract class could be used to build enumerations that hold non-integral types, and made it easy to add comments for Visual Studio to present. With .NET 2.0, we're able to take things to a more general state.

Background

Microsoft provides us with the enum class. It actually does a lot of nice things for us when working with enumerations. Enumerations are great for controlling the values developers may select for a property or field. Enumerations provide a good way to manage constants in an application.

Issue

Unfortunately, the .NET enumeration only supports integral types, not other types like strings. Luckily for me and those I work with, we get to work with many different types and objects. We also like to work with constants. We need an enumeration that works with other data types. To make it even tougher, sometimes the values of an enumeration need to be determined at runtime.

What can we do?

C#, with the .NET libraries, provides us with a solution. It involves inheritance, a little Reflection, and now Generics. The results are simple to work with, and can be extended as needed.

The approach

Should I try to extend C#'s enumeration, enum, or start over? I chose to start over. I wanted to make it easy to declare and document. I wanted to have the code handy to make changes. Could it be done with inheritance, Generics, and Reflection so that I could write something like:

C#
/// <summary>
/// Enumeration with values A,B,C
/// </summary>
[Serializable()]
public class TestStringEnumClassABC : GenericEnumClass<string>
{
    /// <summary>
    /// A for A
    /// </summary>
    public static readonly string A = "A";
    /// <summary>
    /// B for B
    /// </summary>
    public static readonly string B = "B";
    /// <summary>
    /// C for C
    /// </summary>
    public static readonly string C = "C";
}

When I want to set a value to "A", I wanted to write code like:

C#
TestStringEnumClassABC test = new TestStringEnumClassABC;
test = "A"; //LOOK NO PARSE ENUM!!! And its a STRING!!!!
//or
test = TestStringEnumClassABC.A;

Looks easy enough.

How can we make this happen?

The approach taken here builds upon Enum Redo. We had two approaches: one where the constants and values of the enum were stored with each instance for functions to work with. The other approach held the constants and values of the enum in static HybridDictionarys, so that they were a part of the class. In Enum Redo, using a HybridDictionary to store constants and the constant names worked well. Using Generics, it's even easier to store the different kinds of constants. In Enum Redo, the constants and their names are collected by using Reflection and stored in arrays. The arrays are stored in the HybridDictionarys using the enum's type for the key or in private arrays. This makes it easy to retrieve the constants and names for an enum.

C#
/// <summary>
/// Using key collection to hold an array by child type.
/// This way the values only need to load once.
/// </summary>
private static HybridDictionary _values = new HybridDictionary();
/// <summary>
/// Holds the names of the values per child type.
/// This way the value names only need to load once.
/// </summary>
private static HybridDictionary _names = new HybridDictionary();

Since we're using Generics, the concept of having a Null value and a Not applicable value didn't make much sense as we had in Enum Redo. However, we still needed a way to store a value and know when a value has been assigned to it.

C#
/// <summary>
/// Holds the current value.
/// </summary>
private T _value; 
/// <summary>
/// The enum value.
/// </summary>
public T Value
{
   get
   {
      if (_isValueSet == true)
      {
           return _value;
      }
      else
      {
           throw new ApplicationException("Value not set");
      }
   }
   set
   {
        _isValueSet = false;
        if (value == null)
        {
           throw new NoNullAllowedException("Not allowed to assign null value.");
        }
        //get the array to work with from the collection
        T[] values = (T[])_values[this.GetType().FullName];
        IComparable comp;
        foreach (T v in values)
        {
            comp = (IComparable)v;
            if (comp.CompareTo(value)==0)
            {
               _value = value;
               _isValueSet = true;
               break;
            }
        }
        if (_isValueSet == false)
        {                    
              throw new ArgumentOutOfRangeException(value.ToString() 
                    + " not found in enumeration.");
        }
    }
}

/// <summary>
/// Holds if value for instance has been set
/// </summary>
private bool _isValueSet = false;
/// <summary>
/// Indicates if the enum's value has been set.
/// </summary>
public bool IsValueSet
{
    get { return _isValueSet ; }
}

In the above code snippet, we can see the use of Generics and the hybrid dictionaries in action. The hybrid dictionaries are static, and so a part of the class, not the instance. We see it used to declare _value and the property Value. The Value property will be used to assign a value to the enum. We can also see how a few touchy generic situations are handled. Since we're generic, we really don't know if the type is a struct, a class, or even an object. We do need a way to compare an incoming value in the setting of Value against the enum's constants. To enforce the ability to compare, the GenericEnumClass class uses a generic constraint to ensure the class has the ICompare interface.

C#
[Serializable()]
public abstract class GenericEnumClass<T> where T: IComparable 

I've added the property IsValueSet as a nice way to know if a value has been assigned to the enum.

To make the GenericEnumClass a little easier to work with, a new constructor was added and the constructor code from the previous version refactored into its own method. The new constructor would allow the enum to be instantiated with a value.

C#
/// <summary>
/// Constructor that must be called to set internal array of values.
/// </summary>
public GenericEnumClass()
{
   Initialize();
}

/// <summary>
/// Constructor where the value may be set.
/// </summary>
/// <param name="setValue">value to set to.</param>
public GenericEnumClass(T setValue)
{
    Initialize();
    this.Value = setValue;
}

/// <summary>
/// Set internal arrays and dictionaries to hold values.
/// </summary>
private void Initialize()
{
    //get the name of the class
    Type thisType = this.GetType();

    //check if have _values loaded
    if (_values.Contains(thisType.FullName) == false)
    {
         //use reflection to get values
         FieldInfo[] fields = 
           thisType.GetFields(BindingFlags.Public | BindingFlags.Static);
         int numFields = fields.Length;

         T[] values = new T[numFields];
         string[] names = new string[numFields];
         IComparable comp;
                
         Type checkType = typeof(T);

         for (int i = 0; i < numFields; i++)
         {
              if (fields[i].IsInitOnly == true && fields[i].FieldType == checkType)
              {
                  //Need to check if value already loaded
                  T addValue = (T)fields[i].GetValue(null);
                        
                  for (int j = 0; j < values.Length; j++)
                  {
                     if (addValue == null)
                     {
                         throw new NotSupportedException("Value of null not supported.");
                     }

                     if (addValue != null && values[j] != null)
                     {
                          comp = (IComparable)values[j];
                          if (comp.CompareTo(addValue) == 0)
                          {
                              throw new NotSupportedException
                ("Value with duplicate names not supported.");
                          } //no dups
                     }
                  } //cycle through them
                  values[i] = addValue; //(DateTime)fields[i].GetValue(null) ;
                  names[i] = fields[i].Name;
             } //get readonly 
             if (fields[i].IsInitOnly == true && fields[i].FieldType != checkType)
             {
                  throw new ArrayTypeMismatchException("A constant with a different type" 
                    + " from the value type has been declared. Look for " 
                    + fields[i].FieldType.Name);
             } //check for mismatch in data type
         } //loop through Public fields


         //values should now be loaded so put in static list
         _values.Add(thisType.FullName, values);
         _names.Add(thisType.FullName, names);
     }
}

The default constructor now calls the Initialize method. The overloaded constructor now calls the Initialize method and sets _value through the Value property to ensure the passed in value is allowed.

The Initialize function hasn't changed much from Enum Redo's constructors. Basically, it has been converted to use Generics. Notice how the Initialize method does not allow nulls. The big reason for not allowing nulls is some types don't hold null values. Also, how can you compare to a null value? Basically, null values present a problem. In various locations, we need to know the type the enumeration holds. Normally, we could use the GetType method of a declared variable. However, only declared variables that have an assigned value or default value will work with GetType. When working with Generics, you don't know if the variable will be assigned. So, we can't use GetType with a variable, and we can't even use Type.GetType(). Luckily, C# has typeof to help with determining the type of a generic. Read-only and constants are stored in static HybridDictionarys.

Many of the 'helper' classes needed some adjusting:

C#
/// <summary>
/// Two dimensional object array with first dimension as the "row",
/// the second dimension containing the "column".
/// Column 0 is name, Column 1 is the value
/// </summary>
/// <returns>two dimensional string array</returns>
public object[,] GetNamedValues()
{
    T[] values = (T[])_values[this.GetType().FullName];
    string[] names = (string[])_names[this.GetType().FullName];
    //create two dimensional array
    object[,] namedValues = new object[values.Length,2];
    
    for (int i =0;i<values.Length ; i++ )
    {
        namedValues[i,0] = (string)names[i] ;
        namedValues[i,1] = (T)values[i];
    }

    return namedValues;
}

As you can see above, the code has been changed to support Generics.

Regarding the helper functions, they all still worked once changed. GetValues returns all the values. GetNames returns the names associated to values. GetNamedValues returns a two dimensional array with names and values. GetNamesValuesTable is the same as GetNamedValues, but puts them into a DataTable. GetNameForValue returns the name associated with a value.

Giving a little up

In order to have the ability to set the value of the enum on instantiation, the class inheriting GenericEnumClass needs to write out the constructors as seen below.

C#
/// <summary>
/// Generic Enumeration with date values A,B,C
/// </summary>
[Serializable()]
public class TestGenericEnumClassABC : GenericEnumClass<DateTime>
{
    /// <summary>
    /// Constructor
    /// </summary>
    public TestGenericEnumClassABC() : base(){}

    /// <summary>
    /// Constructor that sets the value upon instantiation
    /// </summary>
    public TestGenericEnumClassABC(DateTime setValue) : base(setValue) { }

If no constructor is declared, the default constructor of the base is called. Most find this simple to implement. Maybe a future version of C# will change a little so that all constructors of an inherited class just work.

Performance

To help with my analysis of these classes, I used an Open Source tool called Zanebug. It can be found at www.adapdev.com. It can do a lot of cool things that NUnit can't do right now, and it's fairly easy to work with.

In my testing, I concerned myself with object creation time, time to set a value, and memory usage. The DateEnum and StringEnum classes have been converted from Enum Redo, with the null value and not available value removed. This allowed the testing to be a little more accurate in comparing the different enums. Using Zanebug, it pretty much confirmed that the StringEnum for one instance would work faster for instantiation, work faster for setting values, and use the most memory. StringEnumClass, after the first instantiation, took much less time to create per instance, took longer to set a value, and used less memory. StringEnum took 45s to instantiate 1,000,000 times. StringEnumClass took 0.99s to instantiate 1,000,000 times. StringEnum took 0.0867s for a million assignments, while StringEnumClass took 1.096s for a million assignments on my wimpy laptop. So, there's definitely a give and take when it comes to working with the different approaches. Now, when working with the generic enum using string type, a million instantiations of the generic with string or date took 1.08s, and setting a value took 1.1645s for a million string assignments. Is one approach better than the other? Depends on what the application needs to perform. Luckily, both approaches and the generic approach are easy for the coder to implement, and help make application development quicker and more accurate.

Setting read-only values

The use of read-only values adds a good deal of flexibility. These values can be altered at instantiation. The test class TestGenericStringEnumClassInit provides an example of setting the values at run time.

C#
/// <summary>
/// Static constructor to initialize read-only fields
/// </summary>
static TestGenericStringEnumClassInit()
{
    A = "A";
    B = "B";
    C = "C";
}

/// <summary>
/// A for A
/// </summary>
public static readonly string A;
/// <summary>
/// B for B
/// </summary>
public static readonly string B;
/// <summary>
/// C for C
/// </summary>
public static readonly string C;

Notice how none of the read-only fields are set. To set them, we used a static constructor with some simple code. The static constructor could have included code to retrieve values from a file or database. Essentially, this provides a nice technique to associate database values from constants in code at runtime.

History

None yet.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
United States United States
A biography in this little spot...sure.
I've worked at GTE HawaiianTel. I've worked at Nuclear Plants. I've worked at Abbott Labs. I've consulted to Ameritech Cellular. I've consulted to Zurich North America. I've consulted to International Truck and Engine. Right now, I've consulted to Wachovia Securities to help with various projects. I've been to SHCDirect and now at Cision.

During this time, I've used all kinds of tools of the trade. Keeping it to the more familier tools, I've used VB3 to VB.NET, ASP to ASP/JAVASCRIPT/XML to ASP.NET. Currently, I'm developing with C# and ASP.NET. I built reports in Access, Excel, Crystal Reports, and Business Objects (including the Universes and ETLS). Been a DBA on SQL Server 4.2 to 2000 and a DBA for Oracle. I've built OLTP databases and DataMarts. Heck, I've even done Documentum. I've been lucky to have created software for the single user to thousands of concurrent users.

I consider myself fortunate to have met many different people and worked in many environments. It's through these experiences I've learned the most.

Comments and Discussions

 
GeneralRe: Don't Understand Pin
Tim Schwallie16-May-07 7:52
Tim Schwallie16-May-07 7:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.