Click here to Skip to main content
Email Password   helpLost your password?

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:

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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralThanks, but I have never seen such code style
Karel Kral
4:51 11 Apr '08  
Hello,
thanks for useful library.

But your code style ignores almost of all framework rules.
* Do you dislike XML comments so much that you must convert them into normal comments?
* What about formatting "one parameterer of method on separate line and comma also on separate line"?

I have never seen such code in C#.
GeneralRe: Thanks, but I have never seen such code style
PIEBALDconsult
9:01 11 Apr '08  
Karel Kral wrote:
Do you dislike XML comments so much that you must convert them into normal comments?


Those are XML comments.


Karel Kral wrote:
What about formatting "one parameterer of method on separate line and comma also on separate line"?


Shorter lines, easier to find a parameter, etc.


Karel Kral wrote:
I have never seen such code in C#.


It'll catch on someday... or not.
GeneralRe: Thanks, but I have never seen such code style
deerchao
19:01 11 Jun '08  
Me too.
I found it helpful to build a dll, and then use Reflector to get the new source code Smile
GeneralRe: Thanks, but I have never seen such code style
Karel Kral
20:58 11 Jun '08  
Ahh! Great idea! I have reformated all this code by hands Frown
GeneralRe: Thanks, but I have never seen such code style
leppie
9:37 15 Aug '08  
Hehe, I thought I was the only one that read code like that Smile

xacc.ide - now with TabsToSpaces support
IronScheme - 1.0 alpha 4a out now (29 May 2008)

GeneralVery usefull...
Gonzalo Brusella
4:50 12 Mar '08  
You got my 5!

I'm on a Fuzzy State: Between 0 an 1

GeneralRe: Very usefull...
PIEBALDconsult
6:11 16 Mar '08  
Glad to be of service.
GeneralSimple extension: GetDescriptions
Roefes
4:27 19 Feb '08  
Very useful indeed. I've added another handy method, GetDescriptions(), that returns all descriptions for the enum. (Should you want to populate a combobox with the descriptions, etc.)


public static string[] GetDescriptions<T>()
{
List<string> strResults = new List<string>();

T[] values = GetValues<T>();
foreach (T value in values)
{
strResults.Add(GetDescription(value as System.Enum));
}

return (strResults.ToArray());
}

GeneralRe: Simple extension: GetDescriptions
Gonzalo Brusella
4:54 12 Mar '08  
In order to keep all things as generics as possible i've made some changes:

public static IList GetDescriptions()
{
IList strResults = new List();

T[] values = GetValues();
foreach (T value in values)
{
strResults.Add(GetDescription(value as System.Enum));
}

return strResults;
}


I'm on a Fuzzy State: Between 0 an 1

GeneralGreat
merlin981
1:32 19 Feb '08  
Great article, and very useful. 5 from me



~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rhabot - World of Warcraft Bot
Atlantis WoW - Free Private World of Warcraft Server
Make long URLs short with NeatURL.net
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

GeneralRe: Great
PIEBALDconsult
15:54 19 Feb '08  
Glad to be of service.
GeneralTwo things
dgon
3:30 18 Feb '08  
Before anything else, I think its a very nice and useful piece of code.
But there are a couple of things that can be improved (in my opinion):
* Generics usage: I found the library could be even easier to use, safer and some code can be saved if you made the LibEnum class generic, and all your System.Enum arguments turned into generic arguments of that type. Check the internet on other enum libraries to check how we can constraint the generic argument for Enums. Dictionaries must be generic as well.
Another suitable place could be the GetAlternate and setting a constraint on the TAttribute to inherit from Attribute. That way, the compiler will tell you, instead of checking the type at runtime
* Changing the signature of GetAlternate to receive only the property name and constructing the PropertyInfo inside the method.

Once we do these changes, GetDescription can be turned very easily into a GetAlternate merhod with suitable Attribute and property name. We remove code duplication but we add an extra lookup in the Dictionary (something I can live with)

Hope my comments are constructive.

thanks for the code
GeneralRe: Two things [modified]
PIEBALDconsult
6:27 18 Feb '08  
dgon wrote:
how we can constraint the generic argument for Enums


I haven't seen how; the best I know is to constrain to struct and that isn't good enough.



LibEnum is really only a stepping stone on the path to EnumTransmogrifier, which addresses some of your concerns.


I hadn't considered making a generic static class, but that doesn't seem worthwhile anyway:

LibEnum<MyEnum>.Parse("blah")

doesn't seem any better than

LibEnum.Parse<MyEnum>("blah")

on the other hand, you could use an alias to save a few keystrokes

using MyEnumLib = LibEnum ;
MyEnumLib.Parse("blah")

(And as many of you know, I don't use the using directive.)

However, LibEnum does contain some non-generic methods; making the class generic would make them needlessly generic.

And it would mean a separate Dictionary for each enum (and property) in use rather than sharing one amongst all enums in use. (If you want separate dictionaries use an EnumTransmogrifier.)


I prefer the technique of accepting a PropertyInfo over accepting the attribute type and property name separately. Partly because retrieving the property when given the Attribute type and Property name requires Reflection; I put that burden on the caller.

dgon wrote:
instead of checking the type at runtime


But I would still have to throw an exception if there is no property with the provided name.
I haven't found a way of not having a runtime check of one or the other.




The separation of GetDescription and GetAlternateText is because I don't need to validate the Attribute in GetDescription, but now that I look at it again, I think I could add a private method that they could both call.

Edit: I have submitted an update that does that.

modified on Monday, February 18, 2008 10:16 PM

GeneralRe: Two things
dgon
21:39 18 Feb '08  
You are right in your view of generics usage. And yes, in C# you can't specify a enum constraint Frown
But making the class generic just means having separate dictionaries for each enum, not for each enum and attribute, as each attribute would mean an entry in the generic argument dictionary instance. By the way, having different dictionaries is handled automatically by the CLR, so no change on code required.

Passing the burden of using reflection to the caller does not eliminate the need of using it and, just in my opinion, makes the usage less clear (and again, maybe it's just a matter of taste)

Nice stuff
GeneralRe: Two things
PIEBALDconsult
15:54 19 Feb '08  
dgon wrote:
making the class generic just means having separate dictionaries for each enum, not for each enum and attribute,


I meant if I made it generic on both enum and attribute as I thought you had hinted.


dgon wrote:
having different dictionaries is handled automatically by the CLR


Right, but if I had to handle it I could have one dictionary. Big Grin


dgon wrote:
Passing the burden of using reflection to the caller does not eliminate the need of using it


The caller can get (and store) one reference to the property and pass it to several of my methods and constructors, rather than having my code hit reflection each time. And who knows what else the caller may want to do with it?
GeneralDid you try to use an enum member for the attribute?
Steve Hansen
23:47 17 Feb '08  
    [EnumDefault(MyEnum.Test)]
public enum MyEnum
{
Test,
Test2
}

[AttributeUsage(AttributeTargets.Enum, Inherited = false, AllowMultiple = false)]
public sealed class EnumDefaultAttribute : Attribute
{
public EnumDefaultAttribute(object value)
{
Value = value;
}

public object Value
{
get;
private set;
}
}


This compiles without a problem for me. Great helpers though, got my 5.


GeneralRe: Did you try to use an enum member for the attribute?
PIEBALDconsult
4:46 18 Feb '08  
I get

"
An attribute argument must be a constant expression, typeof expression or array creation expression
"

I think yours succeeds because you're passing it as an object, which I don't want to do.
Passing in a System.Enum should be allowed. To do so with the code as submitted, just cast it to one of the allowed types.

[EnumDefault((int)MyEnum.Test)]

I'll think about it more, but I don't think opening the constructor up to allow all objects makes sense.
GeneralWell done!
tarasn
6:23 17 Feb '08  
Nice library .You got my 5.Thanks

DevIntelligence.com - My blog for .Net Developers

GeneralRe: Well done!
PIEBALDconsult
4:21 18 Feb '08  
Glad to be of service.
GeneralNon-generic methods
FrozenCow_
3:38 17 Feb '08  
Nice library. There's one thing that doesn't seem right to me:
All generic methods don't have an 'overload' with the enum-type as argument. Your library includes the method:
bool IsDefined<T>()
But no non-generic method is included like this:
bool IsDefined(Type t)
This means you can't choose the enum-type at runtime. I think it would be nice if these non-generic 'overloads' would also be included in the LibEnum class.
GeneralRe: Non-generic methods
PIEBALDconsult
4:20 18 Feb '08  
Those are in System.Enum, mine just wrap them. A wrapper that doesn't provide anything new seems silly to me.


Last Updated 24 Feb 2008 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010