If you've ever had a list of string constants to maintain, use, and compare, you've probably experienced frustration at the fact that even at .NET 2.0, we don't have a native enum for strings. Further, you can't inherit from the
System.Enum class and so can't extend it to support strings.
StringEnum would be very handy in defining possible database values, holding an enumerable list of string values for populating lists, etc. Many workarounds have been created, but they all have their issues.
Just a warning: the implementation may be a little rough since I just wanted to get something out for people to see and start using right away. That said, feedback is welcome.
I've seen several different methods of achieving something like a true string enum. They are as follows:
Enum.ToString - So why not just use a simple
Enum and then just use the
Parse functions to convert to and from strings?
- Advantages include being easy to define in addition to most of the usual Enum advantages.
- The main disadvantage is that the name is strictly tied to its value. A name like "
PF_SCR_ATTRIB_WIDESCREEN" is unnecessarily decorated, and "SA" is unnecessarily abbreviated. They can be better expressed as "Widescreen" or "Sales", respectively. (I know XML comments can mitigate, but we're working for clear, concise code.) Also, you must use the
Parse functions every time you want to work with the string value. Also, all sorting happens by numeric value and the enum class cannot be extended.
- Attributes - In this method, certain custom attributes are defined and attached to a regular
Enum. (A good example of this can be found here.)
- Since it is an
Enum, advantages are all the advantages of
Enum -- can get a list of values, can parse name from string, etc.
- Drawbacks are that a function must be explicitly called to "mine" the attributes to parse a value or return one, and since the function is universal, you have no benefits of Intellisense until you manually type the name of the function, then of the enum, and a ".". Also, the base type is still a number, and so assignments and comparisons are still done on a numeric level.
- Constants - Using some lesser-known techniques (used in the
StringEnum code), defining a set of constants on a class is actually a fairly good method.
- The advantage is that since your enum values are all string constants, all your comparisons are string comparisons.
- A disadvantage is that string comparisons are just plain old string comparisons relying on
Option Compare or lots of
StrComp for case sensitivity. Also, there is no type checking, and no unnamed values are allowed.
- Structure - An example of this is available here.
- This is one of the best ways since a structure behaves like a value type, which makes it conceptually easy to track. Operators can be overloaded, and you can implement your own functions to consume the structure.
- However, with structures, there is no inheritance, and all the code to extend the string enum must be copied around--difficult if you overload more than the
Equals function. The other problem is that a structure is really not necessary since all you end up doing is copying around references to the
ReadOnly string values. Might as well just pass around a reference to a class.
Using the Code
StringEnumBase provides an inheritable base that provides functionality comparable to the
It has the following advantages:
- Type checking.
- Intellisense pop-up whenever comparison happens.
- Custom mouse-over that indicates name and value.
- Controlled comparison when basic operators are used. Case insensitive by default. Can make case sensitive by assigning a provided attribute to the inheriting class.
- Can accept values outside of the named values. Can limit to named values only by assigning a provided attribute to the inheriting class.
- Can assign a description to each named value using
System.ComponentModel.Description and get it through the base class'
- Provides all of the other relevant functions of the
System.Enum class (e.g.,
Inherit it in the following manner, supplying the type name of the inheritor as the generic type. This is so that the base class has a way to get the type of the class that's inheriting--used to get attributes, fields, etc. You must also declare the parameterized constructor so that it can remain private and parameterized.
completionlist XML comment to get the Intellisense pop-up. The
StringEnumRegisteredOnly attribute is one of the optional attributes to modify the behavior of
''' <completionlist cref="Numbers" />
Public Class Numbers
Inherits StringEnumBase(Of Numbers)
Private Sub New(ByVal StrValue As String)
You can then enumerate a value as a
Shared ReadOnly field or property. I prefer using a field since it fits on one line. You can also use the
DescriptionAttribute in the declaration.
<Description("This is test value one.")> _
Public Shared ReadOnly One As New Numbers("ONE")
<Description("This is test value two.")> _
Public Shared ReadOnly Property Three() As Numbers
Return New Numbers("TWO")
Points of Interest
Looks like some parts of .NET are still rough, even after so many years--especially in VB.NET. For example, in
DebuggerDisplayAttribute, the postfix "nq" is documented as stripping the quotes from the value that it postfixes. The only problem is that it works only for C#. It looks like the inline conditional also works only for C#.
I also stumbled across the XML comment
completionlist. Why isn't this incredibly handy feature documented anywhere?
- 2007-05-02 - Initial release.
- 2007-05-22 - Revision to be more in line with the expected behavior and to add a little functionality.
- Added more comparison operator functions to handle situations where a
StringEnum is compared directly to a string or another object that can be converted to a string (including a different type of
StringEnum). This allows for comparison against a string without the necessity of converting the string to the
StringEnum type, which might cause an error if the
StringEnum is set to accept only registered values.
- Added a narrowing conversion to a string. The fact that it is declared narrowing is important in that other objects will be converted to
StringEnum will be converted to a string, allowing us, in some situations, to control case-sensitivity.
- Implemented the
IConvertible interface so that
StringEnum will be automatically converted to a string; like, for example, when it is passed to a function that is expecting a string argument. This is important since
CType is not automatically called in every situation.
- Modified the
GetValues() function to return a typed array rather than the generic
- Added functions to parse the description to get the
- Added shared properties that return whether or not a particular
StringEnum is using the behavior-modifying attributes.