Introduction
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.
A 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.
Background
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 ToString
and 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 ToString
and 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
The StringEnumBase
provides an inheritable base that provides functionality comparable to the System.Enum
class.
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' .ToDesc
function.
- Provides all of the other relevant functions of the
System.Enum
class (e.g., GetValues
, Parse
, CompareTo
, GetNames
, etc.).
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.
Add the completionlist
XML comment to get the Intellisense pop-up. The StringEnumRegisteredOnly
attribute is one of the optional attributes to modify the behavior of StringEnumBase
.
<StringEnumRegisteredOnly()> _
Public Class Numbers
Inherits StringEnumBase(Of Numbers)
Private Sub New(ByVal StrValue As String)
MyBase.New(StrValue)
End Sub
...
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
Get
Return New Numbers("TWO")
End Get
End Property
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?
History
- 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
before 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 System.Array
.
- Added functions to parse the description to get the
StringEnum
value.
- Added shared properties that return whether or not a particular
StringEnum
is using the behavior-modifying attributes.