Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Enum Utilities

0.00/5 (No votes)
24 Feb 2008 1  
Various methods for working with enumerations

Introduction

In this article I will discuss some classes I've written to simplify working with enumerations. The primary thrust of these classes is added functionality, but in some cases there are performance improvements as well.

EnumDefaultValueAttribute

This class is in response to a shortcoming of the default keyword as it applies to enums. The default keyword was added in C# 2.0 to support generics.

Every variable has a default value: for class references it's null, for value types it's 0, for structs it's an instance with each field set to its default.

An enum is a value type, so its default is 0; but a particular enum may not have a member with value 0, so defaulting to 0 seems inappropriate.

A simple solution is to use System.Enum.GetValues to be sure the value is in the enumeration...

MyEnum x = (MyEnum) System.Enum.GetValues ( typeof(MyEnum) ).GetValue ( 0 ) ;

... but this doesn't give us much control over which value is used.

An attribute seems like a good solution to this situation. At first, applying an attribute to the particular member may seem like the right approach...

enum MyEnum
{
    [EnumDefaultValueAttribute]
    Value1 = 1
,
    Value2 = 2

...
}

... and indeed it could be done that way, but there's no protection against the attribute being applied to more than one member.

Another limitation is if your enum has the FlagsAttribute and you want the default to be a value that doesn't match a member (unless you want to allow multiple attributes and OR their values together).

I chose to have the attribute apply to the enum instead. What would be best would be to have the enum member as the value for the attribute...

[EnumDefaultValueAttribute(MyEnum.Value1)]
enum MyEnum
{
    Value1 = 1
,
    Value2 = 2

...
}

... but enums aren't allowed as parameters to attribute constructors.

So with this approach we have to use a cast or literal value:

[EnumDefaultValueAttribute((int)MyEnum.Value1)]
enum MyEnum
{
    Value1 = 1
,
    Value2 = 2

...
}

or

[EnumDefaultValueAttribute(1)]
enum MyEnum
{
    Value1 = 1
,
    Value2 = 2

...
}

or

[EnumDefaultValueAttribute("Value1")]
enum MyEnum
{
    Value1 = 1
,
    Value2 = 2

...
}

A limitation of this approach is that there is no compile-time checking of the value (and its type), but that's really no worse than what the default keyword does.

GetDefaultValue<T>

The EnumDefaultValueAttribute class contains a static method to assist in accessing the value:

bool   b ;
MyEnum x ;

b = GetDefaultValue<MyEnum> ( out x ) ;

The return value is a boolean indicating whether or not the value comes from the attribute. If the enum doesn't have an EnumDefaultValueAttribute then the technique described above is used.

Accessing attributes requires reflection, which is costly (many have said that but have not provided any numbers to back it up). Here are the results of calling GetDefaultValue<MyEnum> (one million times) before and after I added the use of a dictionary to cache the results:

LibEnum.Default<weekday>     : Sunday               Elapsed= 36630

LibEnum.Default<month>        : January              Elapsed=   313
LibEnum.Default<weekday>     : Sunday               Elapsed=   297

(These results are taken from EnumDemo2 which is discussed later.)

LibEnum

Most of the members of LibEnum offer only a simplified syntax for accessing the members of System.Enum rather than a performance benefit, but I've also included some methods that don't exist in System.Enum.

GetUnderlyingType<T>

System.Type t = LibEnum.GetUnderlyingType<MyEnum>() ;

GetNames<T>

string[] names = LibEnum.GetNames<MyEnum>() ;

GetName

string s = LibEnum.GetName ( MyEnum.Value1 ) ;

Format

string s = LibEnum.Format ( MyEnum.Value1 , "00" ) ;

GetValues<T>

MyEnum[] values = LibEnum.GetValues<MyEnum>() ;

IsDefined<T>

LibEnum contains a generic version of IsDefined and also adds a version that allows for selecting case-insensitivity:

bool b ;

b = LibEnum.IsDefined<MyEnum> ( "Value1" ) ;         // Case-sensitive

b = LibEnum.IsDefined<MyEnum> ( "Value1" , false ) ; // Case-sensitive
b = LibEnum.IsDefined<MyEnum> ( "Value1" , true  ) ; // Case-insensitive

Parse<T>

The built-in way to convert a string to an enum value is with the System.Enum.Parse method:

// Case-sensitive
MyEnum x = (MyEnum) System.Enum.Parse ( typeof(MyEnum) , "Value1" ) ;         

// Case-sensitive
MyEnum x = (MyEnum) System.Enum.Parse ( typeof(MyEnum) , "Value1" , false ) ; 

// Case-insensitive
MyEnum x = (MyEnum) System.Enum.Parse ( typeof(MyEnum) , "Value1" , true ) ;  

With C# 2.0, it is possible to write generic wrapper methods to hide these details, so LibEnum contains two such methods:

MyEnum x = LibEnum.Parse<MyEnum> ( "Value1" ) ;         // Case-sensitive

MyEnum x = LibEnum.Parse<MyEnum> ( "Value1" , false ) ; // Case-sensitive
MyEnum x = LibEnum.Parse<MyEnum> ( "Value1" , true ) ;  // Case-insensitive

TryParse<T>

.NET 2.0 also adds TryParse to many types that have Parse, but System.Enum didn't get them, so LibEnum has them:

MyEnum x ;

LibEnum.TryParse<MyEnum> ( "Value1" , out x ) ;         // Case-sensitive

LibEnum.TryParse<MyEnum> ( "Value1" , false , out x ) ; // Case-sensitive
LibEnum.TryParse<MyEnum> ( "Value1" , true  , out x ) ; // Case-insensitive

GetDescription

There are also times when some text other than the member's name is desired for outputting a user-friendly string when using an enum value. A System.ComponentModel.DescriptionAttribute is commonly used for this:

enum MyEnum
{
    [System.ComponentModel.DescriptionAttribute("The first value")]
    Value1 = 1
,
    [System.ComponentModel.DescriptionAttribute("The second value")]
    Value2 = 2

...
}

LibEnum has a GetDescription method to simplify retrieving these values:

string s = LibEnum.GetDescription ( MyEnum.Value1 ) ;

GetAlternateText

GetAlternateText is similar to GetDescription, but you can specify the attribute and property for the source:

string s = LibEnum.GetAlternateText
(
    MyEnum.Value1
,
    typeof(SomeAttribute).GetProperty ( "PropertyName" )
) ;

Performance Issues

The first group of lines below is from GetDescription and GetAlternateText before I added the caching of the values in a dictionary. The second group of lines is from after; I'll leave the analysis up to you.

GetDescription1              : Humpday              Elapsed= 46588
GetDescription2              : Humpday, TGIF        Elapsed= 74064
GetAlternateText1            : Humpday              Elapsed= 52950
GetAlternateText2            : Humpday, TGIF        Elapsed= 87416

GetDescription1              : Humpday              Elapsed=  1106
GetDescription2              : Humpday, TGIF        Elapsed=  1146
GetAlternateText1            : Humpday              Elapsed=  1644
GetAlternateText2            : Humpday, TGIF        Elapsed=  1706

(These results are taken from EnumDemo2 which is discussed later.)

Default<T>

LibEnum also contains a Default<T> method; all it does is calls EnumDefaultValueAttribute.GetDefaultValue<T> and ignores the returned boolean:

MyEnum x = LibEnum.Default<MyEnum>() ;

EnumTransmogrifier<T>

An application may conceivably parse and output many enum-based values during a run. As with getting the default value, repeatedly accessing the attributes on the values can be costly. Clearly, accessing these values once per run and storing them for later use has the potential to improve the performance of some applications considerably. Additionally, parsing values from attribute properties (for this discussion I'll refer to them as aliases) rather than names may be desirable in some cases.

The EnumTransmogrifier was designed to address these concerns; it may be a bit on the heavy side, as it contains a generic dictionary for each type of lookup, but enums tend to have relatively few members, so each dictionary shouldn't be prohibitively large.

The properties Values, Names, and Aliases give read only access to the underlying data.

The BaseType and DefaultValue are also cached and made available so repeated calls to reflection are not required for those. You may also set the DefaultValue if doing so makes sense in your application.

Constructors

The simplest way to instantiate an EnumTransmogrifier<T> is:

EnumTransmogrifier<MyEnum> MyEnumHelper = 
        new PIEBALD.Types.EnumTransmogrifier<MyEnum>() ;

This constructor will fill the dictionaries with the values, names, and any DescriptionAttribute values found.

In some cases the enum has some attribute other than DescriptionAttribute that you want to use. Simply specify the desired property of that attribute to the constructor:

EnumTransmogrifier<MyEnum> MyEnumHelper = new PIEBALD.Types.EnumTransmogrifier<MyEnum>
(
    typeof(SomeAttribute).GetProperty ( "PropertyName" )
) ;

(The default is typeof(System.ComponentModel.DescriptionAttribute).GetProperty ( "Description" ).)

If you want parsing to be case-insensitive, you may provide a System.Collections.Generic.IEqualityComparer<string> for use by the Alias dictionary:

EnumTransmogrifier<MyEnum> MyEnumHelper = new PIEBALD.Types.EnumTransmogrifier<MyEnum>
(
    System.StringComparer.CurrentCultureIgnoreCase
) ;

(The default is System.StringComparer.CurrentCulture.)

Both defaults may be overridden:

EnumTransmogrifier<MyEnum> MyEnumHelper = new PIEBALD.Types.EnumTransmogrifier<MyEnum>
(
    typeof(SomeAttribute).GetProperty ( "PropertyName" )
,
    System.StringComparer.CurrentCultureIgnoreCase
) ;

Additionally, the constructors allow you to provide a list of aliases to use. This can be handy when the enum doesn't have a suitable attribute, when you want to override aliases from the attribute, and when the enum has the FlagsAttribute and you want to apply an alias to a value that does not have a member.

These aliases must be provided as KeyValuePairs:

EnumTransmogrifier<MyEnum> MyEnumHelper = new PIEBALD.Types.EnumTransmogrifier<MyEnum>
(
    new System.Collections.Generic.KeyValuePair<MyEnum,string> 
            ( MyEnum.Value1 , "One" )
,
    new System.Collections.Generic.KeyValuePair<MyEnum,string> 
            ( MyEnum.Value2 , "Two" )
) ;

Providing a null or empty string value will remove an alias (the name will remain):

EnumTransmogrifier<MyEnum> MyEnumHelper = new PIEBALD.Types.EnumTransmogrifier<MyEnum>
(
    new System.Collections.Generic.KeyValuePair<MyEnum,string> 
            ( MyEnum.Value1 , "" )
,
    new System.Collections.Generic.KeyValuePair<MyEnum,string> 
            ( MyEnum.Value2 , null )
) ;

Parse

Parse will search the aliases for the string value provided.
Parse will throw System.ArgumentException if the value is not found, otherwise it will return the value.
There is also an indexer that wraps Parse.

MyEnum x = MyEnumHelper.Parse ( "Value1" ) ;

MyEnum y = MyEnumHelper [ "The first value" ] ;

Note: This Parse method is slightly slower than System.Enum.Parse, so its primary benefit is the ability to parse aliases.

TryParse

TryParse will search the aliases for the string value provided.
TryParse will return false and the out parameter will be set to the DefaultValue if the value is not found, otherwise it will return true and the out parameter will be set to the value.

MyEnum x ;

MyEnumHelper.TryParse ( "Value1" , out x ) ;

MyEnumHelper.TryParse ( "The first value" , out x ) ;

ToString

ToString is the inverse of Parse; pass in a value and get the alias.
If the value doesn't have an alias, then the member's name will be returned.
If there is no alias and no member (e.g. when using the FlagsAttribute) then the usual ToString is applied to the value.
There is also an indexer that wraps ToString.

string s = MyEnumHelper.ToString ( MyEnum.Value1 ) ;

string t = MyEnumHelper [ MyEnum.Value2 ] ;

Clarification on Parse, TryParse, and ToString

The Alias dictionary contains the member names as well as the aliases, so if the name of a member is passed in to be parsed it will be found in the Alias dictionary.

During Parsing, if the string isn't in the Alias dictionary (e.g. when using the FlagsAttribute) System.Enum.Parse will be performed and, if successful, the string and value will be added to the dictionaries.

During ToStringing, if the value isn't in the Value dictionary (e.g. when using the FlagsAttribute) value.ToString will be performed and, if successful, the value and string will be added to the dictionaries.

The Demo Programs

These demo programs rely on the included Month and Weekday enumerations. (I apologize for any misspellings of non-English month names.)

EnumDemo1

EnumDemo1 just demonstrates instantiating EnumTransmogrifiers, accessing the properties, Parsing, and ToStringing. I'm not sure how well it'll behave on a system that is not set to English.

EnumDemo2

EnumDemo2 performs a number of functions on the enumerations (one million times each) and displays the elapsed time (in milliseconds).

The output from a sample run (Win XP SP2, .net 2.0, Pentium 4 3GHz, 1GB):

LibEnum.Default       : January              Elapsed=   313
LibEnum.Default     : Sunday               Elapsed=   297

System.Enum.Parse            : Sunday               Elapsed=  1188
PIEBALD.Lib.LibEnum.Parse    : Sunday               Elapsed=  1190
MonthHelper.Parse            : Sunday               Elapsed=  1389

weekday.ToString1            : Sunday               Elapsed= 17142
WeekdayHelper.ToString1      : Sunday               Elapsed=  1115
weekday.ToString2            : Sunday, Saturday     Elapsed= 17502
WeekdayHelper.ToString2      : Weekends             Elapsed=  1119
weekday.ToString3            : Monday, Tuesday      Elapsed= 17543
WeekdayHelper.ToString3      : Monday, Tuesday      Elapsed=  1131

GetDescription1              : Humpday              Elapsed=  1106
GetDescription2              : Humpday, TGIF        Elapsed=  1146
GetAlternateText1            : Humpday              Elapsed=  1644
GetAlternateText2            : Humpday, TGIF        Elapsed=  1706

The first two lines reflect accessing the default values (discussed above).

The next three lines are parsing operations: Notice that System.Enum.Parse is the fastest and the generic wrapper for it is only slightly slower. The dictionary-based parse is a little slower, but it provides the benefit of parsing aliases.

The next six lines are ToString operations. I am quite surprised that the normal ToString of an enum value is so much slower than the dictionary-based ToString. (I've found that ToStringing an enum that doesn't have the FlagsAttribute is just as slow.) Judging by this, if you do a lot of ToStringing of enum values, use a dictionary.

The last four lines were discussed earlier.

Using the Code

The zip file contains:

  • EnumDefaultValueAttribute.cs
  • EnumTransmogrifier.cs
  • LibEnum.cs
  • build.bat
  • csc.rsp
  • EnumDemo1.cs
  • EnumDemo2.cs
  • EnumDump.cs
  • MonthEnum.cs
  • PolyglotAttribute.cs
  • WeekdayEnum.cs

Once you extract the files to a directory you should be able to execute build.bat to compile the demo programs. (They are console applications.)

To use the methods in your own projects, simply add the appropriate files.

History

  • 2008-02-15: First submitted
  • 2008-02-18: Reworked implementations of GetDescription and GetAlternateText

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