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 enum
s. 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 struct
s 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 enum
s 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" ) ;
b = LibEnum.IsDefined<MyEnum> ( "Value1" , false ) ;
b = LibEnum.IsDefined<MyEnum> ( "Value1" , true ) ;
Parse<T>
The built-in way to convert a string
to an enum
value is with the System.Enum.Parse
method:
MyEnum x = (MyEnum) System.Enum.Parse ( typeof(MyEnum) , "Value1" ) ;
MyEnum x = (MyEnum) System.Enum.Parse ( typeof(MyEnum) , "Value1" , false ) ;
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" ) ;
MyEnum x = LibEnum.Parse<MyEnum> ( "Value1" , false ) ;
MyEnum x = LibEnum.Parse<MyEnum> ( "Value1" , true ) ;
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 ) ;
LibEnum.TryParse<MyEnum> ( "Value1" , false , out x ) ;
LibEnum.TryParse<MyEnum> ( "Value1" , true , out x ) ;
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 enum
s 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 default
s 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 ToString
ing, 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 ToString
ing. 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 ToString
ing an enum
that doesn't have the FlagsAttribute
is just as slow.) Judging by this, if you do a lot of ToString
ing 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