Click here to Skip to main content
Click here to Skip to main content

Enum Utilities

, 24 Feb 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
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<month>       : January              Elapsed=   313
LibEnum.Default<weekday>     : 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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

PIEBALDconsult
Software Developer (Senior)
United States United States
BSCS 1992 Wentworth Institute of Technology
 
Originally from the Boston (MA) area. Lived in SoCal for a while. Now in the Phoenix (AZ) area.
 
OpenVMS enthusiast, ISO 8601 evangelist, photographer, opinionated SOB, acknowledged contrarian
 
---------------
 
"If you need help knowing what to think, let me know and I'll tell you." -- Jeffrey Snover [MSFT]
 
"Typing is no substitute for thinking." -- R.W. Hamming
 
"I find it appalling that you can become a programmer with less training than it takes to become a plumber." -- Bjarne Stroustrup
 
ZagNut’s Law: Arrogance is inversely proportional to ability.
 
"Well blow me sideways with a plastic marionette. I've just learned something new - and if I could award you a 100 for that post I would. Way to go you keyboard lovegod you." -- Pete O'Hanlon
 
"linq'ish" sounds like "inept" in German -- Andreas Gieriet
 
"Things would be different if I ran the zoo." -- Dr. Seuss
 
"Wrong is evil, and it must be defeated." – Jeff Ello
 
"A good designer must rely on experience, on precise, logical thinking, and on pedantic exactness." -- Nigel Shaw
 
“It’s always easier to do it the hard way.” -- Blackhart

“If Unix wasn’t so bad that you can’t give it away, Bill Gates would never have succeeded in selling Windows.” -- Blackhart

"Omit needless local variables." -- Strunk... had he taught programming
 

 
"We learn more from our mistakes than we do from getting it right the first time."
 
My first rule of debugging: "If you get a different error message, you're making progress."
 
My golden rule of database management: "Do not unto others' databases as you would not have done unto yours."
 
My general rule of software development: "Design should be top-down, but implementation should be bottom-up."

Comments and Discussions

 
GeneralThanks, but I have never seen such code style PinmemberKarel Kral11-Apr-08 4:51 
GeneralRe: Thanks, but I have never seen such code style PinmemberPIEBALDconsult11-Apr-08 9:01 
GeneralRe: Thanks, but I have never seen such code style Pinmemberdeerchao11-Jun-08 19:01 
GeneralRe: Thanks, but I have never seen such code style PinmemberKarel Kral11-Jun-08 20:58 
Ahh! Great idea! I have reformated all this code by hands Frown | :-(
GeneralRe: Thanks, but I have never seen such code style Pinmemberleppie15-Aug-08 9:37 
GeneralVery usefull... PinmemberGonzalo Brusella12-Mar-08 4:50 
GeneralRe: Very usefull... PinmemberPIEBALDconsult16-Mar-08 6:11 
GeneralSimple extension: GetDescriptions PinmemberRoefes19-Feb-08 4:27 
GeneralRe: Simple extension: GetDescriptions PinmemberGonzalo Brusella12-Mar-08 4:54 
GeneralGreat Pinmembermerlin98119-Feb-08 1:32 
GeneralRe: Great PinmemberPIEBALDconsult19-Feb-08 15:54 
GeneralTwo things Pinmemberdgon18-Feb-08 3:30 
GeneralRe: Two things [modified] PinmemberPIEBALDconsult18-Feb-08 6:27 
GeneralRe: Two things Pinmemberdgon18-Feb-08 21:39 
GeneralRe: Two things PinmemberPIEBALDconsult19-Feb-08 15:54 
QuestionDid you try to use an enum member for the attribute? PinmemberSteve Hansen17-Feb-08 23:47 
AnswerRe: Did you try to use an enum member for the attribute? PinmemberPIEBALDconsult18-Feb-08 4:46 
GeneralWell done! Pinmembertarasn17-Feb-08 6:23 
GeneralRe: Well done! PinmemberPIEBALDconsult18-Feb-08 4:21 
GeneralNon-generic methods PinmemberFrozenCow_17-Feb-08 3:38 
GeneralRe: Non-generic methods PinmemberPIEBALDconsult18-Feb-08 4:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 24 Feb 2008
Article Copyright 2008 by PIEBALDconsult
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid