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

Optimized Enum.ToString()

, 6 Nov 2011
Rate this:
Please Sign up or sign in to vote.
A generic version of Enum, which provides much more faster formatting

Introduction

While for most projects this is not an issue, some developers might notice that simply converting enumeration value into a string takes quite a lot of processor time. This is especially important if you develop a high-loaded server application and need to get enumeration names thousands sometimes per request. The class described in this article solves the problem once for any enum.

Note that the method Enum.ToString() is very often called implicitly, for example in the following statements:

"Paint it " + Color.Black
Console.Write("Paint it {0}", Color.Black)

The Problem

In order to find the string representation of an enumeration value, the .NET Framework uses Reflection, which is known to be quite an expensive operation. Also, it uses a universal approach to all enumerations, checking if it was declared with FlagsAttribute and so on. It also results in excessive memory consumption, because new strings are allocated whenever ToString is called.

The simplest and quickest solution is to declare an array of strings containing string representations of all values in the enumeration. For example, for the simple enumeration:

enum Color
{
  Red,
  Green,
  Blue
}

The array will look like this:

private static readonly string[] colorNames = new[] {"Red", "Green", "Blue"};

In order to convert the Color value to a string, we should simply cast the enumeration value to int and take an array value by that index:

string myColor = colorNames[(int) color];

The standard implementation of Enum.ToString() returns a number if the value is not defined. So our code should do array bounds checking.

int i = (int)value;
string myColor = i >= 0 && i < colorNames.Length ? colorNames[i] : i.ToString();

Actually, you don't need to type the array items manually, you can simply assign it the result of the Enum.GetNames() method:

private static readonly string[] colorNames = Enum.GetNames(typeof(Color));

While this approach is really the fastest, it has some limitations:

  1. It can be applied only to very simple enumerations. It won't work with "sparse" enumerations, or enumerations with the FlagsAttribute attribute. If you want to improve performance of a sparse enumeration, you should use a dictionary instead of the array (which is a bit less fast). Converting flags to string is quite a complicated operation.
  2. It requires additional coding for every enumeration you want to format.

Another popular solution is using the switch statement. It also performs very well and it supports any type of enumeration, however it requires even more manual coding and keeping the enumerations and formatting methods in sync.

The Solution

The goal was to create a universal method, which does exactly what Enum.ToString() does, but efficiently.

In order to generalize the approach and make it as fast as possible for any enumeration type, I made use of an interesting feature provided by generics: static constructor of a generic type is called once for each actual type used as a typed parameter, and each specialization of a generic type uses its own set of static fields. So we can declare a generic Enum type and save all the information required to perform enum formatting in its static members. We can also apply the Strategy design pattern to select the best strategy for each enumeration type. Here is the class declaration:

/// <summary>
/// Helper class for enum types
/// </summary>
/// <typeparam name="T">Must be enum type (declared using
///          the <c>enum</c> keyword)</typeparam>
public static class Enum<t> where T : struct, IConvertible

(I wish .NET had a constraint for enums.)

In my static class, I consider three kinds of enumerations:

  • Simple - when named constants take all consecutive values from 0 to any N.
  • Sparse - when named constants are not consecutive, or do not start from zero (basically almost any enum with initializer).
  • Flagged - marked with the FlagsAttribute attribute. I use the array of strings to store names for simple enums, Dictionary<int,string> to store names for sparse enums, and dictionary + array of values as unsigned integers for flagged enums.

Take a look at the static constructor:

static Enum()
{
    Type type = typeof(T);
    if (!type.IsEnum)
        throw new ArgumentException("Generic Enum type works only with enums");
    string[] names = Enum.GetNames(type);
    var values = (T[])Enum.GetValues(type);
    if (type.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0)
    {
        Converter = new FlagsEnumConverter(names, values);
    }
    else
    {
        if (values.Where((t, i) => Convert.ToInt32(t) != i).Any())
        {
            Converter = new DictionaryEnumConverter(names, values);
        }
        if (Converter == null)
            Converter = new ArrayEnumConverter(names);
    }
}

The classes ArrayEnumConverter, DictionaryEnumConverter, and FlagsEnumConverter are nested classes derived from the single abstract class EnumConverter. Converter is a static field of type EnumConverter.

For the sake of performance, the class Enum supports getting an enum value name not only by enumeration value, but also by the respective integer value. The corresponding methods simply call the method of the saved EnumConverter instance.

/// <summary>
/// Converts enum value to string
/// </summary>
/// <param name="value">Enum value converted to int</param>
/// <returns>If <paramref name="value"/> is defined,
/// the enum member name; otherwise the string representation
/// of the <paramref name="value"/>.
/// If <see cref="FlagsAttribute"/> is applied,
/// can return comma-separated list of values</returns>
public static string ToString(int value)
{
    return Converter.ToStringInternal(value);
}

/// <summary>
/// Converts enum value to string
/// </summary>
/// <param name="value">Enum value</param>
/// <returns>If <paramref name="value"/> is defined,
/// the enum member name; otherwise the string representation
/// of the <paramref name="value"/>.
/// If <see cref="FlagsAttribute"/> is applied,
/// can return comma-separated list of values</returns>
public static string ToString(T value)
{
    return Converter.ToStringInternal(value.ToInt32(null));
}

Using the methods is almost as simple as using the standard implementation (although it won't be called implicitly).

string myColor = Enum<Color>.ToString(Color.Blue);
//or
string myColor = Enum<Color>.ToString((int)Color.Blue);
//or
string myColor = Enum<Color>.ToString(2);

What About Parse?

Obviously the same approach can be used to parse enumeration values. The faster way to do so is to use the internal Dictionary<string,int> to quickly find values by enumeration names. However as it wasn't the goal of the article in the attached source code, I used the same collections as for the ToString() methods and simply enumerate by them. I also added the ability to restrict parsing an arbitrary integer value into an undeclared enumeration value, which is not supported in the standard implementation. It also contains the methods TryParse, so if you use .NET 2.0, you might find them useful.

public static T Parse(string value, bool ignoreCase = false, bool parseNumeric = true)
{
    return (T) Enum.ToObject(typeof(T), 
      Converter.ParseInternal(value, ignoreCase, parseNumeric));
}

public static bool TryParse(string value, bool ignoreCase, 
       bool parseNumeric, out T result)
{
    int ir;
    bool b = Converter.TryParseInternal(value, ignoreCase, parseNumeric, out ir);
    result = (T) Enum.ToObject(typeof(T), ir);
    return b;
}

public static bool TryParse(string value, bool ignoreCase, out T result)
{
    int ir;
    bool b = Converter.TryParseInternal(value, ignoreCase, true, out ir);
    result = (T)Enum.ToObject(typeof(T), ir);
    return b;
}

public static bool TryParse(string value, out T result)
{
    int ir;
    bool b = Converter.TryParseInternal(value, false, true, out ir);
    result = (T)Enum.ToObject(typeof(T), ir);
    return b;
}

Therefore the class EnumConverter is declared as follows:

abstract class EnumConverter
{
    public abstract string ToStringInternal(int value);
    public abstract int ParseInternal(string value, bool ignoreCase, bool parseNumber);
    public abstract bool TryParseInternal(string value, 
           bool ignoreCase, bool parseNumber, out int result);
}

Performance

I wrote a simple performance test to compare the performance of my class and the standard .NET implementation. In the test, I used these three enumerations:

enum Simple
{
    Zero,
    One,
    Two,
    Three,
    Four,
    Five
}

enum Sparse
{
    MinusOne = -1,
    One = 1,
    Three = 3,
    Four,
    Five,
    Hundred = 100,
}

[Flags]
enum Flagged
{
    None = 0,
    One = 1,
    Two = 2,
    Both = 3,
    Four = 4,
    All = 7
}

In the test, I simply called different implementations of the same methods 1,000,000 times and wrote the results to the console.

Here are the results of converting the value Simple.Four to a string using five different approaches:

Operation                                                Time  Ratio Result
Simple.Four.ToString()                                   2059   1,00 Four
Enum<Simple>.ToString(Simple.Four)                        343   6,00 Four
Enum<Simple>.ToString(4)                                   26  79,19 Four
SimpleToStringUsingSwitch(Simple.Four)                     13 158,38 Four
SimpleToStringUsingArray(Simple.Four)                      16 128,69 Four

The results show that manual approaches are still the best by performance, however our overload that accepts an integer is a close runner-up and works almost 80 times faster than the standard version! The overload that accepts an enumeration works 6 times faster, which is also much better.

The column Result shows that each approach produces the same result.

Test results for other values of arguments and other enumeration types:

Operation                                                Time  Ratio Result
((Simple) 404).ToString()                                2096   1,00 404
Enum<Simple>.ToString((Simple) 404)                       618   3,39 404
Enum<Simple>.ToString(404)                                285   7,35 404
Sparse.Four.ToString()                                   2007   1,00 Four
Enum<Sparse>.ToString(Sparse.Four)                        382   5,25 Four
Enum<Sparse>.ToString(4)                                   94  21,35 Four
((Sparse) 404).ToString()                                2162   1,00 404
Enum<Sparse>.ToString((Sparse) 404)                       688   3,14 404
Enum<Sparse>.ToString(404)                                342   6,32 404
Flagged.Four.ToString()                                  5011   1,00 Four
Enum<Flagged>.ToString(Flagged.Four)                      388  12,91 Four
Enum<Flagged>.ToString(4)                                  99  50,62 Four
((Flagged) 404).ToString()                               5585   1,00 404
Enum<Flagged>.ToString((Flagged) 404)                    1138   4,91 404
Enum<Flagged>.ToString(404)                               800   6,98 404
(Flagged.Two|Flagged.Four).ToString()                    5301   1,00 Two, Four
Enum<Flagged>.ToString(Flagged.Two|Flagged.Four)         1302   4,07 Two, Four
Enum<Flagged>.ToString(6)                                 949   5,59 Two, Four
Flagged.All.ToString()                                   5021   1,00 All
Enum<Flagged>.ToString(Flagged.All)                       398  12,62 All
Enum<Flagged>.ToString(7)                                  99  50,72 All

I didn't optimize the parsing methods very much, however they also performed faster than the standard implementation in most cases.

Operation                                                Time  Ratio Result
Enum.Parse(typeof(Simple), "Four")                       1191   1,00 Four
Enum<Simple>.Parse("Four")                                670   1,78 Four
Enum.Parse(typeof(Simple), "4")                          1193   1,00 Four
Enum<Simple>.Parse("4")                                   791   1,51 Four
Enum.Parse(typeof(Sparse), "Four")                       1126   1,00 Four
Enum<Sparse>.Parse("Four")                                799   1,41 Four
Enum.Parse(typeof(Sparse), "4")                          1184   1,00 Four
Enum<Sparse>.Parse("4")                                   806   1,47 Four
Enum.Parse(typeof(Flagged), "Four")                      1184   1,00 Four
Enum<Flagged>.Parse("Four")                              1114   1,06 Four
Enum.Parse(typeof(Flagged), "4")                         1229   1,00 Four
Enum<Flagged>.Parse("4")                                  948   1,30 Four
Enum.Parse(typeof(Flagged), "Two,Four")                  1517   1,00 Two, Four
Enum<Flagged>.Parse("Two,Four")                          1835   0,83 Two, Four

Despite the class using Int32 internally, it works well with any underlying type except Int64 and UInt64. This is a certain limitation, which can be easily got over, however I personally have never needed to use an enumeration of that size, so I didn't implement it.

Source Code

In the attachment, you will find the source code of the generic Enum class and the performance test. Any thoughts on the implementation will be very much appreciated.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

ideafixxxer
Software Developer (Senior) EPAM Systems
Canada Canada
No Biography provided

Comments and Discussions

 
QuestionGood article ... additional extension method PinmemberEnrique Albert10-Feb-13 23:20 
GeneralRe: Good article ... additional extension method Pinmemberideafixxxer11-Feb-13 0:12 
GeneralMy vote of 5 PinmemberEdo Tzumer10-Jan-13 5:05 
GeneralMy vote of 5 PinmemberAddy Tas5-Feb-12 7:26 
QuestionGreat job PinmemberTom Clement22-Dec-11 14:28 
GeneralRe: Great job Pinmemberideafixxxer22-Dec-11 23:38 
Questionvery nice PinmemberCIDev14-Dec-11 10:00 
GeneralRe: very nice Pinmemberideafixxxer15-Dec-11 4:10 
GeneralMy vote of 5 Pinmemberlinuxjr17-Nov-11 13:52 
GeneralMy vote of 5 PinprotectorMarc Clifton13-Nov-11 2:05 
GeneralMy vote of 5 PinmemberPhilip Liebscher7-Nov-11 12:59 
Question"(I wish .NET had a constraint for enums.)" Pinmemberspringy766-Nov-11 23:42 
AnswerRe: "(I wish .NET had a constraint for enums.)" Pinmemberkornman007-Nov-11 5:24 
QuestionAnother idea PinmemberRobert Rohde6-Nov-11 9:21 
AnswerRe: Another idea Pinmemberideafixxxer6-Nov-11 10:01 
GeneralMy vote of 5 PinmemberEugene Sadovoi6-Nov-11 6:29 
QuestionWhat about Int64 Pinmemberkornman006-Nov-11 2:46 
AnswerRe: What about Int64 Pinmemberideafixxxer6-Nov-11 6:05 

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 | Mobile
Web03 | 2.8.140814.1 | Last Updated 6 Nov 2011
Article Copyright 2011 by ideafixxxer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid