Click here to Skip to main content
15,885,278 members
Articles / Programming Languages / C#
Article

Enum Redo

Rate me:
Please Sign up or sign in to vote.
2.77/5 (5 votes)
24 Nov 20056 min read 40.6K   125   15   8
Another approach to enums that even works for non-integral types like 'string'.

Introduction

Do you enjoy typing enum.parse to convert values in enumerators? Want to use other data types for your enumerator? Do you like to have the actual values customizable at run-time? Like it to be easy to document the values using C# commenting? Yeah? Well here's another way to look at the enumerators.

Background

Microsoft has provided us with the enum class. It actually does a lot of nice things when working with the enumerators. Unfortunately, it supports only integral types except char. Luckily for me, I could work with many different types and objects. At times, I really want to treat specific values of these types like constants. Also, the enum.parse just irked me.

Issues

C# didn't come with the enumerator types to support other data types and to have runtime generated values.

What can we do?

C# along with the .NET libraries provide us with a solution. It involves inheritance and a bit of reflection. The results are simple to work with and can be extended as and when needed.

The approach

Should I try to extend enum or start over again? 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 and reflection so that I could write something like:

C#
/// <summary>
/// Enumerator with value A,B,C
/// </summary>
[Serializable()]
public class TestStringEnumCommonABC : StringEnumCommon
{
    /// <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", it would write the code like:

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

Looks easy enough.

How can we make this happen?

The approach taken here uses an abstract class that has a few methods already defined. We're also using .NET 1.1. It was declared as an abstract to force the developer to inherit from it. The class has a few static members:

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();
private static HybridDictionary _names = new HybridDictionary();
private string _value = "" ; //Don't allow null
/// <summary>
/// StringNull is actually zero length string
/// </summary>
public readonly string StringNull = ""; //we don't like nulls!
/// <summary>
/// StringNotApplicable is 'NOTAPPLICABLE'
/// </summary>
public readonly string StringNotApplicable = "NOTAPPLICABLE";

_values is a type member, it will hold the values that the coder creates in the child class. I went with the HybridDictionary, it looked as if it would work well. If you know that your project would use the base class more than ten times, you can switch it to a Hashtable. While it would have been nice to have a static array hold the values for each child class inherited from EnumeratorCommon, the static values are shared by all the children in .NET 1.1. _names will hold the names associated with the values in the child classes. Regarding the value for each instance, I figured I'd let .NET work for me. So, _value will actually hold the value at the instance level. StringNull and StringNotApplicable are my way of dealing with two critical logical points most developers and architects inappropriately address. Perhaps, I'll get around to write an article for them later. Notice _value, StringNull, and StringNotApplicable have been declared of type string.

I did have a choice on how to manage the values created by the developer in the child class:

  1. Use reflection over and over again when working with the values.
  2. Associate the values with each instance.
  3. Associate the values with the type.
  4. Associate the values with each child type.

1. looked too slow. 2. actually held some promise and I left the code for it in the sample project. It just took up too much memory and required a little extra processing. 3. is a happy medium. 4. may actually be possible, but still may require some interfaces, reflection, and coder intervention to make it work. How does a parent set a value in a child class? How do you enforce that the child class properly implements certain members? Again I would use interfaces and abstract properties. It may still be possible, but I decided to settle on approach 3. I'm sure there are others and hopefully we'll find out about those later.

So, how and when do we load the values from the child. I decided to use the constructor in EnumCommon to hold the logic that loads _values. This can be varied, but at least this provides the basic code:

C#
/// <summary>
/// Constructor that must be called 
/// to set internal array of values.
/// </summary>
public StringEnumCommon()
{
    //get the name of the class
    Type t = this.GetType();
    
    //check if have _values loaded
    if (_values.Contains(t.FullName) == false)
    {
        
        //use reflection to get values
    
        FieldInfo[] fields = 
          t.GetFields(BindingFlags.Public | BindingFlags.Static );
        int numFields = fields.Length;
        string[] values = new string[numFields +2]; 
            //need two more for StringNull and StringNotApplicable
        string[] names = new string[numFields + 2]; 
            //need two more for StringNull and StringNotApplicable
        for (int i =0; i<numFields ; i++ )
        {
            if (fields[i].IsInitOnly == true && 
                       fields[i].FieldType == _value.GetType() )
            {
                //Need to check if value already loaded
                string addValue = (string)fields[i].GetValue(null) ;
                for ( int j= 0; j < values.Length; j++)
                {
                    if ( (string)values[j] == addValue)
                    {
                        throw new NotSupportedException("Value " + 
                             "with duplicate names not supported.");
                    } //no dups
                } //cycle through them
                values[i] = 
                      addValue; //(string)fields[i].GetValue(null) ;
                names[i] = fields[i].Name;
            } //get readonly 
            if (fields[i].IsInitOnly == true && 
                           fields[i].FieldType != _value.GetType() )
            {
                throw new ArrayTypeMismatchException("A " + 
                        "constant with a different type from "
                        + "the value's type has been declared. Look for "
                        + fields[i].FieldType.Name );
            } //check for mismatch in data type
        } //loop through Public fields
        //in last two spots put Null and NotApplicable
        values[values.Length-2] = StringNull;
        names[values.Length-2] = "StringNull";
        values[values.Length-1] = StringNotApplicable;
        names[values.Length-1] = "StringNotApplicable";

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

First the constructor gets the type, which at this point is the child class' type. It then checks to see if _values already contains the values for the child type. If it does not, reflection is used to cycle through the public, static fields and fields that are set to readonly (IsInitOnly). Other checks are done to ensure that the value is loaded once and to make sure the type of the field matches the type of _value. If the value from the field looks good, it's loaded into a string array. Once cycling through the fields is completed, the array is placed in _values with the child type as the key. At the same time the _names array is loaded with the names associated with the values in the child class.

So why did we load up the values and the names into arrays. Performance! When the value is set, we don't want to allow the coder to set a value that the coder has not declared in the child type. So rather than using slowfection, we can rip through the array. We use the names array to keep it in synch with the values and to help make the helper functions work faster:

C#
/// <summary>
/// The enum value.
/// </summary>
public string Value
{
    get { return _value; }
    set 
    {
        bool found = false;
    
        //check for null or not applicable
        if (value == StringNull || value == StringNotApplicable)
        {
            _value = value;
            found = true;
        }
        else
        {
            //get the array to work with from the collection
            Type t = this.GetType();
            string[] values = 
                  (string[])_values[this.GetType().FullName];
            foreach (string v in values)
            {
                if ( v == value )
                {
                    _value = value;
                    found = true;
                    break;
                }
            }
            if (found == false)
            {
                throw new ArgumentOutOfRangeException(value + 
                                 " not found in enumeration.");
            }
        }
    }
}

Remember _value belongs to the instance, not the class type. _values, which belongs to the abstract class contains the arrays of values that are declared by the coder in the child class. Notice that the values of StringNull and StringNotApplicable are allowed no matter what the coder declares.

From here we can add the code that makes life easier for the coder, the helper functions:

C#
/// <summary>
/// Returns the name of the assigned value.
/// </summary>
public string Name
{
    get {return GetNameForValue(_value);}
}

/// <summary>
/// Returns a clone of the available values.
/// </summary>
public string[] GetValues()
{   //want to return copy
    string[] values = (string[])_values[this.GetType().FullName];
    return (string[])values.Clone();
}

/// <summary>
/// Returns a clone of the available values.
/// </summary>
public string[] GetNames()
{   //use reflection to get values
    string[] names = (string[])_names[this.GetType().FullName];
    return (string[])names.Clone();
    
}
        
/// <summary>
/// Two dimensional string 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 string[,] GetNamedValues()
{
    string[] values = (string[])_values[this.GetType().FullName];
    string[] names = (string[])_names[this.GetType().FullName];
    //create two dimensional array
    string[,] namedValues = new string[values.Length,2];
    for (int i =0;i<values.Length ; i++ )
    {
            namedValues[i,0] = (string)names[i] ;
            namedValues[i,1] = (string)values[i];
    }
            return namedValues;
}

/// <summary>
/// For the supplied value, returns corresponding Name
/// Returns zero length string if value not found.
/// </summary>
public string GetNameForValue(string forValue)
{
    
    string[] values = (string[])_values[this.GetType().FullName];
    string[] names = (string[])_names[this.GetType().FullName];
    
    string name = "";
    for (int i =0;i<values.Length ; i++ )
    {
        if (forValue == (string)values[i])
        {
            name =(string)names[i] ;
        }
    }

    return name;
}

/// <summary>
/// Returns a DataTable with two columns,
/// the first for the Name
/// the second for the Value
/// </summary>
public DataTable GetNamedValuesTable()
{
    string[] values = (string[])_values[this.GetType().FullName];
    string[] names = (string[])_names[this.GetType().FullName];
    //create table
    DataTable dt = new DataTable();
    DataColumn dc = new DataColumn();
    DataRow dr;
    dc.DataType = System.Type.GetType("System.String");
    dc.ColumnName = "Name";
    dc.AutoIncrement = false;
    dc.Caption = "Name";
    dc.ReadOnly = true;
    dc.Unique = true;
    dt.Columns.Add(dc);
    dc = new DataColumn();
    dc.DataType = System.Type.GetType("System.String");
    dc.ColumnName = "Value";
    dc.AutoIncrement = false;
    dc.Caption = "Value";
    dc.ReadOnly = true;
    dc.Unique = true;
    dt.Columns.Add(dc);
    
    for (int i =0;i<values.Length ; i++ )
    {
        dr = dt.NewRow();
        dr["Name"] = (string)names[i] ;
        dr["Value"] = (string)values[i];
        dt.Rows.Add(dr);
    }
    dt.AcceptChanges();
    return dt;
}

GetValues returns all the values. GetNames returns the names associated to the values. GetNamedValues returns a two dimensional array with the name and value. GetNamesValuesTable is the same as GetNamedValues, but puts them into a DataTable. GetNameForValue returns the name associated with a value.

Performance

To help my analysis of these classes, I used an open source tool called Zanebug. It can be found here. It can do a lot of cool things that NUnit can't do right and it's fairly easy to work with.

I made two different versions for managing the values and names. StringEnum has the array of values declared per instance and cycles through the names with reflection. StringEnumCommon uses two hybrid collections declared statically to hold the names and the values.

In my testing, I was concerned with the object creation time, time to set a value, and memory usage. Using Zanebug, it pretty much confirmed that StringEnum for one instance would work faster for instantiation, work faster for setting values, and use more memory. StringEnumCommon after the first instantiation took much less time to create an instance (0.0147 vs. .0008), took longer to set a value (0.00044 vs. .0011), and less memory. So there's definitely a give and take when it comes to working with the different approaches. Is one approach better than the other? It depends on what the application needs to perform. Luckily, both approaches are easy for the coder to implement and help make the application development quicker and more accurate.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


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

 
GeneralThe source is still unavailable Pin
Don Meuse30-Dec-06 7:49
Don Meuse30-Dec-06 7:49 
GeneralRe: The source is still unavailable Pin
Don Meuse30-Dec-06 7:52
Don Meuse30-Dec-06 7:52 
GeneralSource incomplete Pin
shawnkaczmarek6-Jan-06 3:15
shawnkaczmarek6-Jan-06 3:15 
GeneralRe: Source incomplete Pin
Tim Schwallie12-Jan-06 11:07
Tim Schwallie12-Jan-06 11:07 
QuestionWhere's the source? Pin
AxelM24-Nov-05 21:33
AxelM24-Nov-05 21:33 
AnswerRe: Where's the source? Pin
Mikael Wiberg24-Nov-05 23:07
Mikael Wiberg24-Nov-05 23:07 
GeneralRe: Where's the source? Pin
AxelM25-Nov-05 3:04
AxelM25-Nov-05 3:04 
AnswerRe: Where's the source? Pin
Mikael Wiberg25-Nov-05 7:23
Mikael Wiberg25-Nov-05 7:23 

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.