Introduction
The idea of string enumerations in C# (and the lack thereof), has bothered me for a while. Enum's are useful things but only allow the storage of numeric types. What if you want to use strings?
Background
It struck me that I had a number of requirements when using string 'constants'.
I wanted the ability to define the values in-line and reference them like an enum
value - e.g. MyEnum.MyString
. It would also be nice to be able to reference the string values as a particular enum
type, for the purposes of validation and strong-typing.
Options
Constants
The first thing I naturally gravitated towards was constants defined in a specific class. The following shows an example of this:
public sealed class MyConsts
{
private MyConsts() {}
public const string Val1 = "MyVal1";
public const string Val2 = "MyVal2";
public const string Val3 = "MyVal3";
}
The constants can be easily accessed as MyConsts.Val1
etc. This is extremely simple and reasonably neat. As these are constants the values are compiled in and as such can also be used nicely in switch
statements.
Constants do have an interesting side effect in that the values are actually copied to any client code that uses the class. This means that client assemblies could potentially have out of date values if the values in the constants assembly were to change and it were redeployed without a client rebuild.
A possibly preferable alternative to the class would be to use a struct
to house the constants, as this would always be created on the stack rather than the heap.
Static readonly
readonly
values can be initialized with a value and changed only within a class's constructor. This effectively means their eventual value is not known at compile time. The code sample above can be changed slightly to illustrate this 'pseudo const'.
public sealed class MyConsts
{
private MyConsts() {}
public static readonly string Val1 = "MyVal1";
public static readonly string Val2 = "MyVal2";
public static readonly string Val3 = "MyVal3";
}
Values are again accessed in exactly the same way e.g. MyConsts.Val3
, but as the values are conceptually dynamic, they will always be accessed from the class where they are defined. This does also mean that cannot be evaluated in switch
statements.
Enums
Enum's can only contain numeric values right? Well, each value can actually be decorated with custom attributes, which means it's possible to extend a normal enum
to contain string values as follows:
public enum HandTools
{
[StringValue("Cordless Power Drill")
Drill = 5,
[StringValue("Long nose pliers")
Pliers = 7,
[StringValue("20mm Chisel")
Chisel = 9
}
Enums can of course be declared without explicit numeric values, in which case each item will have implicit numeric items (starting from 0). This can be a bit of a pain when debugging (and provides a slight performance hit), so Microsoft suggests always declaring a numeric value.
The StringValue
attribute is a simple attribute class (derived from System.Attribute
) that is constructed with a string and exposes that string via a single Value
property:
public class StringValueAttribute : System.Attribute
{
private string _value;
public StringValueAttribute(string value)
{
_value = value;
}
public string Value
{
get { return _value; }
}
}
We now need some way of accessing these strings (via the StringValue
attribute) and using them as part of our standard enum
type.
StringEnum
The StringEnum
class acts as a wrapper for string value access in enumerations. It assumes that enum
s wishing to expose string
values do so via the StringValue
attribute. The StringEnum
class has static and instance portions and provides the following static methods:
Parse
: Parse a string
value and return the corresponding enum
value.
GetStringValue
: Return the string
value associated with the given enum
value.
IsStringValueDefined
: Indicate the existence or non-existence of an enum
value with the given string
value.
The GetStringValue
is shown below. This takes in an enum
value and performs a fast type lookup before retrieving the StringValue
attribute (if it can find it). The string value is then returned. We use a static Hashtable
(_stringValues
) to cache results (keyed by enum
value) to avoid the minor reflection performance hit on subsequent requests.
public static string GetStringValue(Enum value)
{
string output = null;
Type type = value.GetType();
if (_stringValues.ContainsKey(value))
output = (_stringValues[value] as StringValueAttribute).Value;
else
{
FieldInfo fi = type.GetField(value.ToString());
StringValueAttribute[] attrs =
fi.GetCustomAttributes(typeof (StringValueAttribute),
false) as StringValueAttribute[];
if (attrs.Length > 0)
{
_stringValues.Add(value, attrs[0]);
output = attrs[0].Value;
}
}
return output;
}
Case-insensitive overloads are provided for the Parse
and IsStringValueDefined
methods.
Similar instance methods are provided (using a constructor taking the enum
's type), with an additional method for use in data binding:
GetListValues
: Return an IList
for data binding the StringEnum
values.
Using the code
The code can be simply used by extending any enum
to include [StringValue("")]
attributes. You can then use the StringEnum
class to retrieve the values as follows:
public enum CarType
{
[StringValue("Saloon / Sedan")] Saloon = 5,
[StringValue("Coupe")] Coupe = 4,
[StringValue("Estate / Wagon")] Estate = 6,
[StringValue("Hatchback")] Hatchback = 8,
[StringValue("Utility")] Ute = 1,
}
public void TestMethod()
{
MessageBox.Show(StringEnum.GetStringValue(CarType.Estate));
MessageBox.Show(StringEnum.Parse(typeof(CarType),
"estate / wagon", true).ToString());
int enumValue = (int)StringEnum.Parse(typeof(CarType),
"estate / wagon", true);
MessageBox.Show(enumValue.ToString());
}
Points of interest
The source project has a full set of NUnit tests to illustrate each method, and the demo project shows other items such as examples of data binding with StringEnum
.
Have fun!
History
- (29/07/2005) - Updated source zip file with hashtable caching for
GetStringValue
.