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

A perfect C# string enumerator

Rate me:
Please Sign up or sign in to vote.
2.76/5 (14 votes)
2 Jan 2008CPOL6 min read 68.7K   159   33   19
An excellent and easy way to implement string enumerators in C#.

Introduction

I really love C#, and do my utmost at kicking myself in the rear for not having gotten into it sooner than I did. In spite of that, though, I sorely miss some things from other languages.

Take string enumerators, for example. Enumerators are an amazing thing when it comes to keeping things clean and easy to maintain. You define them once, assign them their values, and use their names throughout the program. Behind the scenes, each enumeration is translated to the value you set it up with, and that's what you end up storing on disk, using for calculations, etc.

String enumerations are no different. You pick a set of names, assign them their text values, and in code, you always refer to the names. When it comes time to using them for disk storage, display, etc., you refer to the enumeration's value.

But in C#, for whatever reason (anyone know why?), this was left out. I've looked around for quite a while, and found proposed solutions that ranged from simply creating a class with static strings to some that use Reflection. They all seemed either too simple or too complex, and none gave me all the functionality an enumerator should have.

So, I set out to make my own, and I think I've done it. I would say that it mimics a regular enumerator to 100%, and provides exactly the same functionality, usage, and behaviour as regular enumerators. I cannot think of anything that has been missed, and am depending on my readers to help point those things out :)

A quick note about the download - it includes an NUnit project used for testing the enumerator. If you do not have NUnit, simply remove the project from the solution, and you'll be good to go. If you do have it and want to run the tests, re-point the nunit.framework reference in the test project to your local copy of nunit.framework.dll, rebuild the solution, run NUnit, and load the included StringEnumerator.nunit project file.

Background

When I set out to create this, I had to sit down and think about all the ways in which an enumerator could be used in order to make sure the string enumerator is as true to its name as possible. I've created a number of tests using NUnit, and so far so good. I can't really think of any more good tests for this, but if anyone else can, please let me know.

Usage

You can refer to the tests in the NUnit project to see the enumerator's capabilities, but here they are in brief:

  • Assigning a static enumerator's value to a string.
  • Instantiating an enumerator object, setting it to a valid enumerator value, passing it as a parameter to a method, and assigning its value to a string.
  • Instantiating an enumerator object, setting it to a valid enumerator value, passing it as a parameter to a method, copying that enumerator to a new one, and assigning the new enumerator's value to a string.
  • Same as the above, but right after copying the first enumerator to the second one, we change the first one's value, and ensure the second one has not been affected. Note that although we are dealing with a reference item, the enumerator class is immutable, so we are always creating a new copy when assigning it a new value. Assigning an existing object to another one is done just with the "=" sign, without a dependency on the ICloneable interface, and the copy is not a shallow one (although there isn't much depth to this class).
  • Testing enumerated strings that contain spaces.
  • Assigning a valid string value to an enumerator object.
  • Assigning an invalid string value to an enumerator object, and getting an exception.
  • Creating two enumerator objects, and ensuring that ==, !=, and Equals() perform properly.

Once you create the enumerator class, you use it exactly the way you would use a regular enumerator, with one slight difference: when you want to deal with the actual string value of either the enumerator object or of one of the static enumerator values, you must use the ToString() method. I would dearly love to find a way around this, but even if I can't, I think it's a pretty small price to pay.

The EStringEnum base class

To create a string enumerator, you'll need two things: the base EStringEnum class, and a class that you define (which inherits from EStringEnum) with the actual string values.

The base class exposes two items that must be overwritten, as well as logic for validating string values that the user is trying to assign to an enumerator object (i.e., the value must be one of the valid enumerated strings), and some overloads for ToString() and various operators:

C#
/// <summary>
/// A string enumerator base class. Must be inherited.
/// </summary>
public abstract class EStringEnum
{
    #region Data
    protected string mCurrentEnumValue = "";
    protected abstract string EnumName { get; }
    protected abstract List<string> PossibleEnumValues { get; }
    #endregion
    #region Constructors
    public EStringEnum() { }
    /// <summary>
    /// A string enumerator
    /// </summary>
    /// <param name="value">A valid enumerator value. An exception is raised 
    /// if the value is invalid.</param>
    public EStringEnum(string value)
    {
        if (PossibleEnumValues.Contains(value))
        {
            mCurrentEnumValue = value;
        }
        else
        {
            string errorMessage = string.Format(
                "{0} is an invalid {1} enumerator value", 
                value, 
                EnumName);
            throw new Exception(errorMessage);
        }
    }
    #endregion
    #region Overloads
    /// <summary>
    /// Returns the enumerator's current value
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return mCurrentEnumValue;
    }
    /// <summary>
    /// Test for equality
    /// </summary>
    /// <param name="stringEnum1"></param>
    /// <param name="stringEnum2"></param>
    /// <returns></returns>
    public static bool operator ==(EStringEnum stringEnum1, EStringEnum stringEnum2)
    {
        return (stringEnum1.ToString().Equals(stringEnum2.ToString()));
    }
    /// <summary>
    /// Test for inequality
    /// </summary>
    /// <param name="stringEnum1"></param>
    /// <param name="stringEnum2"></param>
    /// <returns></returns>
    public static bool operator !=(EStringEnum stringEnum1, EStringEnum stringEnum2)
    {
        return (!stringEnum1.ToString().Equals(stringEnum2.ToString()));
    }
    /// <summary>
    /// Test for equality
    /// </summary>
    /// <param name="o"></param>
    /// <returns></returns>
    public override bool Equals (object o)
    {
        EStringEnum stringEnum = o as EStringEnum;
        return (this.ToString().Equals(stringEnum.ToString()));
    }
    /// <summary>
    /// Retrieve the hashcode
    /// </summary>
    /// <returns></returns>
    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
    #endregion
}

Only two things from this base class must be implemented when inheriting it:

C#
protected abstract string EnumName { get; }
protected abstract List<string> PossibleEnumValues { get; }

EnumName must return a string with the name of the string enumerator (e.g., ECarManufacturers), and PossibleEnumValues must return a List<string> of the enumerated strings (e.g., "Ford", "Toyota", etc.).

The actual enumerator class

The enumerator must inherit from EStringEnum, and do the following:

  • Implement the two properties mentioned above,
  • Provide static accessors for each enumerated string,
  • Provide a constructor (which just calls the base constructor), and
  • Provide an implicit conversion from a string type to an object type of the actual enumerator (e.g. ECarManufacturers).

It may sound complex, but as the example below shows, it really is quite simple to implement:

C#
/// <summary>
/// A car manufacturer enumerator
/// </summary>
public class ECarManufacturers: EStringEnum
{
    #region Data
    /// <summary>
    /// Used by EStringEnum to identify the current class
    /// </summary>
    protected override string EnumName { get { return "ECarManufacturers"; } }
    protected override List<string> PossibleEnumValues 
    { 
        get { return mPossibleEnumValues; } 
    }
    /// <summary>
    /// Complete list of string values that this enumerator can hold
    /// </summary>
    private static List<string> mPossibleEnumValues = new List<string>()
    {
        "Toyota",
        "Honda",
        "Ford",
        "Chrysler",
        "Volvo",
        "General Motors"
    };
    /// <summary>
    /// CarManufacturers type
    /// </summary>
    static public ECarManufacturers Toyota
    {
        get { return new ECarManufacturers(mPossibleEnumValues[0]); }
    }
    /// <summary>
    /// CarManufacturers type
    /// </summary>
    static public ECarManufacturers Honda
    {
        get { return new ECarManufacturers(mPossibleEnumValues[1]); }
    }
    /// <summary>
    /// CarManufacturers type
    /// </summary>
    static public ECarManufacturers Ford
    {
        get { return new ECarManufacturers(mPossibleEnumValues[2]); }
    }
    /// <summary>
    /// CarManufacturers type
    /// </summary>
    static public ECarManufacturers Chrysler
    {
        get { return new ECarManufacturers(mPossibleEnumValues[3]); }
    }
    /// <summary>
    /// CarManufacturers type
    /// </summary>
    static public ECarManufacturers Volvo
    {
        get { return new ECarManufacturers(mPossibleEnumValues[4]); }
    }
    /// <summary>
    /// CarManufacturers type
    /// </summary>
    static public ECarManufacturers GeneralMotors
    {
        get { return new ECarManufacturers(mPossibleEnumValues[5]); }
    }
    #endregion
    #region Constructor
    /// <summary>
    /// A car manufacturer enumerator
    /// </summary>
    /// <param name="value">A valid enumerator value. 
    /// An exception is raised if the value is invalid.</param>
    private ECarManufacturers(string value): base(value)
    { }
    #endregion
    #region Misc methods
    /// <summary>
    /// Implicitly convert a string to a CarManufacturers object
    /// </summary>
    /// <param name="value">A string value to convert to an ECarManufacturers 
    /// enum value. An exception is raised if the value is invalid.</param>
    /// <returns></returns>
    public static implicit operator ECarManufacturers(string value) 
    {
        return new ECarManufacturers(value);
    }
    /// <summary>
    /// Implicitly convert an ECarManufacturers object to a string
    /// </summary>
    /// <param name="carManufacturers">A ECarManufacturers object
    ///        whose value is to be returned as a string 
    /// <returns></returns>
    public static implicit operator string(ECarManufacturers carManufacturers) 
    {
        return carManufacturers.ToString(););
    }
    #endregion
}

Examples

The following tests are included in the NUnit test project (available in the download), but I'm posting them here as well to give you a quick look at how true-to-life this implementation is:

C#
[TestFixture]
public class TestClass1
{
    [Test]
    public void Test01()
    {
        string result = ECarManufacturers.GeneralMotors.ToString();
        string expected = "General Motors";
        Assert.IsTrue(result == expected);
    }
    [Test]
    public void Test02()
    {
        ECarManufacturers carManufacturer = ECarManufacturers.Honda;
        string result = carManufacturer.ToString();
        string expected = "Honda";
        Assert.IsTrue(result == expected);
    }
    [Test]
    public void Test03()
    {
        Test03A(ECarManufacturers.Ford);
    }
    private void Test03A(ECarManufacturers carManufacturer)
    {
        string expected = "Ford";
        string result = carManufacturer.ToString();
        Assert.IsTrue(result == expected);
    }
    [Test]
    public void Test04()
    {
        Test04A(ECarManufacturers.Ford);
    }
    private void Test04A(ECarManufacturers carManufacturer)
    {
        ECarManufacturers tempCarManufacturers2 = carManufacturer;
        string result = tempCarManufacturers2.ToString();
        string expected = "Ford";
        Assert.IsTrue(result == expected);
    }
    [Test]
    public void Test05()
    {
        Test05A(ECarManufacturers.Ford);
    }
    private void Test05A(ECarManufacturers carManufacturer)
    {
        ECarManufacturers carManufacturer2 = carManufacturer;
        carManufacturer = ECarManufacturers.Chrysler;
        string expected1 = "Chrysler";
        string result1 = carManufacturer.ToString();
        string expected2 = "Ford";
        string result2 = carManufacturer2.ToString();
        Assert.IsTrue(result1 == expected1 && result2 == expected2);
    }
    [Test]
    public void Test06()
    {
        ECarManufacturers tempCarManufacturers = "Ford";
        string result = tempCarManufacturers.ToString();
        string expected = "Ford";
        Assert.IsTrue(result == expected);
    }
    [Test]
    public void Test07()
    {
        ECarManufacturers tempCarManufacturers2 = null;
        try
        {
            tempCarManufacturers2 = "Orion";
            Assert.Fail();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.Message.Equals(
                "Orion is an invalid ECarManufacturers enumerator value"));
        }
    }
    [Test]
    public void Test08()
    {
        ECarManufacturers tempCarManufacturers = "General Motors";
        Assert.IsTrue(tempCarManufacturers.ToString().Equals(
            ECarManufacturers.GeneralMotors.ToString()));
    }
    [Test]
    public void Test09()
    {
        ECarManufacturers carManufacturers1 = ECarManufacturers.GeneralMotors;
        ECarManufacturers carManufacturers2 = ECarManufacturers.GeneralMotors;
        Assert.IsTrue(carManufacturers1 == carManufacturers2);
    }
    [Test]
    public void Test10()
    {
        ECarManufacturers carManufacturers1 = ECarManufacturers.GeneralMotors;
        ECarManufacturers carManufacturers2 = ECarManufacturers.Ford;
        Assert.IsTrue(carManufacturers1 != carManufacturers2);
    }
    [Test]
    public void Test11()
    {
        ECarManufacturers carManufacturers1 = ECarManufacturers.GeneralMotors;
        int number = carManufacturers1.GetHashCode();
    }
}

In conclusion

Not a very complex solution when you think about it, and I think it covers all the behaviours an enumerator is expected to have. When it comes time to use the enumerator, you use it exactly like you would use a regular enumerator. If you can think of anything I've missed or can improve upon, feel free to tell me.

Something I'd like to do to improve on:

  • Extract as much logic as possible and put it in the base class (such as the static accessors). I could use something like Spring.Net to inject the accessors at runtime, but this would have two major drawbacks: Intellisense would no longer work, and I would be adding a dependency to the project.

History

  • December 31 2007 - Initial post (Happy new year!).
  • January 01 2008 - Added examples.
  • January 02 2008 - Removed dependency on ToString() to extract the text value.

License

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


Written By
Team Leader
Canada Canada
A developer that's been tinkering with computers since he first laid eyes on his buddy's Atari in the mid 80's and messed around with GWBasic and Logo. He now divides his time among his wife, kids, and evil mistress (a term lovingly [ahem...] given to his computer by the wife ...).

For more info, please see my LinkedIn profile: http://www.linkedin.com/pub/david-catriel/44/b01/382

Comments and Discussions

 
QuestionI think when you say 'enumerator' you mean 'enumeration'? Pin
Spike0xFF15-Feb-11 8:47
Spike0xFF15-Feb-11 8:47 
QuestionChar to String Pin
stixoffire21-Oct-08 10:47
stixoffire21-Oct-08 10:47 
AnswerRe: Char to String Pin
David Catriel21-Oct-08 10:50
David Catriel21-Oct-08 10:50 
GeneralI totally fail to see any use of this too... Pin
Michael Moreno3-Jan-08 11:12
Michael Moreno3-Jan-08 11:12 
AnswerRe: I totally fail to see any use of this too... Pin
David Catriel3-Jan-08 14:46
David Catriel3-Jan-08 14:46 
GeneralRe: I totally fail to see any use of this too... Pin
PIEBALDconsult8-Jan-08 16:27
mvePIEBALDconsult8-Jan-08 16:27 
Generali really don't find this very useful Pin
Seishin#2-Jan-08 21:47
Seishin#2-Jan-08 21:47 
AnswerRe: i really don't find this very useful [modified] Pin
David Catriel3-Jan-08 3:04
David Catriel3-Jan-08 3:04 
I'm sorry you don't find it useful, but am glad you left the question.

There are multiple advantages to this method, and they cannot be achieved by using ToString() on a numeric enumerator (first example that comes to mind - strings with spaces or other characters that are not allowed in field names).

The biggest advantage, in my opinion, is the ability to easily refer to the same string values at both coding time and run time. During coding, the properties are available to you through intellisense, which will show you the static accessors you added to the class. At run time you can then use the properties' textual values to validate user entry, add column names to reports, populate combo boxes, etc.

Look at this for example:
ECarManufacturers CarManufacturers1 = ECarManufacturers.GeneralMotors;<br />
mTextBox.Text = CarManufacturers1;<br />
<br />
ECarManufacturers CarManufacturers2 = "General Motors";<br />
mTextBox.Text = CarManufacturers2;<br />
<br />
// This throws an exception since "Lada" is not a valid ECarManufacturers value<br />
ECarManufacturers CarManufacturers3 = "Lada"; <br />
<br />
mTextBox.Text = ECarManufacturers.GeneralMotors;


You have the ability to create an enumerator object and assign it a string that contains spaces or other characters that are not allowed in field names (note that ECarManufacturers.GeneralMotors actually contains "General Motors") and then assign it to a string field.

You can also assign a particular piece of text directly to the enumerator object, and it will get validated (an exception is thrown if you assign an invalid piece of text).

Finally, you can also directly assign the the textual value of a static enumerator property to a string field.

There are many ways to play with this. I've used string enumerators quite a bit in VB6, and found them extremely useful. Once you start working with this, you realise there are many ways it can come in handy.

This having been said, though, you can also look at the message left eariler regarding an alternate (but similar) solution for framework 3.5 that would probably be smaller and less complex. If this interests you, and you're using that framework, take a look at it.


modified on Thursday, January 03, 2008 9:32:27 AM

GeneralRe: i really don't find this very useful Pin
PIEBALDconsult8-Jan-08 13:25
mvePIEBALDconsult8-Jan-08 13:25 
GeneralRe: i really don't find this very useful Pin
David Catriel8-Jan-08 15:17
David Catriel8-Jan-08 15:17 
GeneralVery nice Pin
mikeperetz2-Jan-08 9:39
mikeperetz2-Jan-08 9:39 
AnswerRe: Very nice Pin
David Catriel2-Jan-08 10:19
David Catriel2-Jan-08 10:19 
Generalthe problemmmmmmm Pin
Thanks for all the fish2-Jan-08 3:21
Thanks for all the fish2-Jan-08 3:21 
GeneralRe: the problemmmmmmm [modified] Pin
David Catriel2-Jan-08 3:43
David Catriel2-Jan-08 3:43 
GeneralRe: the problemmmmmmm Pin
Hotcut2-Jan-08 20:30
Hotcut2-Jan-08 20:30 
GeneralRe: the problemmmmmmm Pin
David Catriel3-Jan-08 7:08
David Catriel3-Jan-08 7:08 
GeneralRe: the problemmmmmmm Pin
PIEBALDconsult8-Jan-08 13:22
mvePIEBALDconsult8-Jan-08 13:22 
GeneralRe: the problemmmmmmm Pin
David Catriel8-Jan-08 15:15
David Catriel8-Jan-08 15:15 
GeneralRe: the problemmmmmmm Pin
PIEBALDconsult8-Jan-08 16:15
mvePIEBALDconsult8-Jan-08 16:15 

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.