Enums – Better syntax, improved performance and TryParse in NET 3.5

Recently I needed to map external data into in-memory objects. In such scenarios the TryParse methods of Int and String are useful but where is Enum.TryParse? TryParse exists in .NET 4.0 but like a lot of people I’m on .NET 3.5.

A quick look at Enum left me scratching my head.

  • Why didn’t enums receive the generic love that collections received in .NET 2.0?
  • Why do I have to pass in typeof(MyEnum) everywhere?
  • Why do I have to the cast results back to MyEnum all the time?
  • Can I write TryParse and still make quick – i.e. without try/catch?

I found myself with a small class, Enum<T> that solved all these. I was surprised when I put it through some benchmarks that also showed the various methods were significantly faster when processing a lot of documents. Even my TryParse was quicker than that in .NET 4.0.

While there is some small memory overhead with the initial class (about 5KB for the first, a few KB per enum after) the performance benefits came as an additional bonus on top of the nicer syntax.

Before (System.Enum)

var getValues = Enum.GetValues(typeof(MyEnumbers)).OfType();
var parse = (MyEnumbers)Enum.Parse(typeof(MyEnumbers), "Seven");
var isDefined = Enum.IsDefined(typeof(MyEnumbers), 3);
var getName = Enum.GetName(typeof(MyEnumbers), MyEnumbers.Eight);
MyEnumbers tryParse;
Enum.TryParse<MyEnumbers>("Zero", out tryParse);

After (Enum)

var getValues = Enum<MyEnumbers>.GetValues();
var parse = Enum<MyEnumbers>.Parse("Seven");
var isDefined = Enum<MyEnumbers>.IsDefined(MyEnumbers.Eight);
var getName = Enum<MyEnumbers>.GetName(MyEnumbers.Eight);
MyEnumbers tryParse;
Enum<MyEnumbers>.TryParse("Zero", out tryParse);

I also added a useful ParseOrNull method that lets you either return null or default using the coalesce so you don’t have to mess around with out parameters, e.g.

MyEnumbers myValue = Enum<MyEnumbers>.ParseOrNull("Nine-teen") ?? MyEnumbers.Zero;

The class

GitHub has the latest version of EnumT.cs

Usage notes

  • This class as-is only works for Enum’s backed by an int (the default) although you could modify the class to use longs etc.
  • I doubt very much this class is of much use for flag enums
  • Casting from long can be done using the CastOrNull function instead of just putting (T)
  • GetName is actually much quicker than ToString on the Enum… (e.g. Enum.GetName(a) over a.ToString())
  • IsDefined doesn’t take an object like Enum and instead has three overloads which map to the actual types Enum.IsDefined can deal with and saves run-time lookup
  • Some of the method may not behave exactly like their Enum counterparts in terms of exception messages, nulls etc.

[)amien

10 responses

  1. Avatar for Ryan

    No love for flag enums? Looks like a useful class otherwise, thanks

    Ryan 17 October 2010
  2. Avatar for Craig Stuntz

    Thanks; this is really useful! Interesting technique: You're taking advantage of generic instantiation to populate a cache for each distinct enum type.

    Craig Stuntz 18 October 2010
  3. Avatar for Damien Guard

    I thought about writing a "How it works" section but decided against it.

    But yes, that's pretty much it :)

    Damien Guard 18 October 2010
  4. Avatar for jmorris

    I took a similar approach creating a Enum type and created a bunch of common helper methods for parsing/converting enum values. The funny thing is that you get so used to the API, that you begin to think that it's native to .net after awhile...well at least you wish it was native to .net!

    Thanks for sharing.

    -Jeff

    jmorris 19 October 2010
  5. Avatar for WaSaMaSa

    Useful class, but instead of Enum use Enum.Metod and you can use syntax sugar with it. Compiler can automatically recognize type parameters of methods not a classes Example: //class type parameter(from aticle) var isDefined = Enum.IsDefined(MyEnumbers.Eight); var getName = Enum.GetName(MyEnumbers.Eight);

    //method type parameter(without auto recognized) var isDefined = Enum.IsDefined(MyEnumbers.Eight); var getName = Enum.GetName(MyEnumbers.Eight);

    //method type parameter(auto recognized) var isDefined = Enum.IsDefined(MyEnumbers.Eight); var getName = Enum.GetName(MyEnumbers.Eight);

    Cheers.

    WaSaMaSa 25 October 2010
  6. Avatar for Damien Guard

    The problem with doing it that way is that you'd end up having to duplicate the cache per method...

    Unless the Enum. methods were just proxies back to Enum...

    Damien Guard 25 October 2010
  7. Avatar for WaSaMaSa

    Oh, I see.

    But you can wrap your class by there methods for using syntax sugar and not duplicate caches :)

    WaSaMaSa 25 October 2010
  8. Avatar for Tom Groves

    Nice one Damo - just used this in a project, really nice idea. Hope all's well Stateside :-)

    Tom Groves 20 November 2010
  9. Avatar for Richard

    Great work ! Just noticed in little difference against original enum methods : original "Parse" performs a Trim() on processed value. Your version doesn't.

    Richard 30 August 2012
  10. Avatar for Richard

    Hi, As I said previously i find your helper nice, but (IMHO) there were a few leaks.

    • Confusing method names (which method takes int value or label ? etc)
    • not behaves the same as original ones
    • no unit tests neither comments

    so I improved it a little bit.

    Here you may find my version, fully tested with below NUnit test (sorry, comments are in french) :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Utils
    {
      public static class EnumHelper where T : struct
      {
        private static readonly IEnumerable All = Enum.GetValues(typeof(T)).Cast();
        private static readonly Dictionary InsensitiveNames = All.ToDictionary(k => Enum.GetName(typeof(T), k), StringComparer.OrdinalIgnoreCase);
        private static readonly Dictionary Names = All.ToDictionary(k => k, v => v.ToString());
        private static readonly Dictionary SensitiveNames = All.ToDictionary(k => Enum.GetName(typeof(T), k));
        private static readonly Dictionary Values = All.ToDictionary(k => Convert.ToInt32(k));
    
        public static T? CastValueOrNull(int value)
        {
          T foundValue;
          if ( Values.TryGetValue(value, out foundValue) )
          return foundValue;
    
          return null;
        }
    
        public static string GetLabelForItem(T value)
        {
          string name;
          Names.TryGetValue(value, out name);
          return name;
        }
    
        public static string[] GetLabels()
        {
          return Names.Values.ToArray();
        }
    
        public static IEnumerable GetItems()
        {
          return All;
        }
    
        public static bool IsDefinedItem(T value)
        {
          return Names.Keys.Contains(value);
        }
    
        public static bool IsDefinedLabel(string value)
        {
          return IsDefinedLabel(value, false);
        }
    
        public static bool IsDefinedLabel(string value, bool ignoreCase)
        {
          if ( ignoreCase )
            return InsensitiveNames.Keys.Contains(value);
          else
            return SensitiveNames.Keys.Contains(value);
        }
    
        public static bool IsDefinedValue(int value)
        {
          return Values.Keys.Contains(value);
        }
    
        public static T ParseLabel(string value)
        {
          T parsed = default(T);
          if ( !SensitiveNames.TryGetValue(value, out parsed) )
            throw new ArgumentException("Value " + value + " is not one of the named constants defined for the enumeration", "value");
          return parsed;
        }
    
        public static T ParseLabel(string value, bool ignoreCase)
        {
          if ( !ignoreCase )
            return ParseLabel(value);
    
          T parsed;
          if ( !InsensitiveNames.TryGetValue(value, out parsed) )
            throw new ArgumentException("Value " + value + " is not one of the named constants defined for the enumeration", "value");
          return parsed;
        }
    
        public static T? ParseOrNullLabel(string value)
        {
          return ParseLabelOrNull(value, false);
        }
    
        public static T? ParseLabelOrNull(string value, bool ignoreCase)
        {
          if ( String.IsNullOrEmpty(value) )
            return null;
    
          T foundValue;
          if ( ignoreCase )
          {
            if ( InsensitiveNames.TryGetValue(value, out foundValue) )
              return foundValue;
          }
          else
          {
            if ( SensitiveNames.TryGetValue(value, out foundValue) )
              return foundValue;
          }
    
          return null;
        }
    
        public static bool TryParseLabel(string value, out T returnValue)
        {
          return SensitiveNames.TryGetValue(value, out returnValue);
        }
    
        public static bool TryParseLabel(string value, bool ignoreCase, out T returnValue)
        {
          if ( !ignoreCase )
            return TryParseLabel(value, out returnValue);
    
          return InsensitiveNames.TryGetValue(value, out returnValue);
        }
      }
    }
    
    namespace Tests
    {
      [TestFixture]
      public class EnumHelperTests
      {
        public enum TestEnum
        {
          Clear = 0,
    
          Valeur1 = 1,
          Valeur2 = 2,
          Valeur3 = 3,
        }
    
        [Test]
        public void IsDefinedTest()
        {
          Assert.IsTrue(EnumHelper.IsDefinedValue(1));
          Assert.IsFalse(EnumHelper.IsDefinedLabel("1"));
    
          Assert.IsTrue(EnumHelper.IsDefinedLabel("Valeur1"));
          Assert.IsFalse(EnumHelper.IsDefinedLabel("valeur1"));
          Assert.IsFalse(EnumHelper.IsDefinedLabel("testquelconque"));
    
          Assert.IsTrue(EnumHelper.IsDefinedLabel("Valeur1", false));
          Assert.IsFalse(EnumHelper.IsDefinedLabel("valeur1", false));
          Assert.IsFalse(EnumHelper.IsDefinedLabel("testquelconque", false));
    
          Assert.IsTrue(EnumHelper.IsDefinedLabel("Valeur1", true));
          Assert.IsTrue(EnumHelper.IsDefinedLabel("valeur1", true));
          Assert.IsFalse(EnumHelper.IsDefinedLabel("testquelconque", true));
    
          Assert.IsTrue(EnumHelper.IsDefinedItem(TestEnum.Valeur1));
        }
    
        [Test]
        public void CastOrNullTest()
        {
          Assert.AreEqual(TestEnum.Valeur1, EnumHelper.CastValueOrNull(1));
          Assert.AreEqual(null, EnumHelper.CastValueOrNull(4));
        }
    
        [Test]
        public void GetNameTest()
        {
          Assert.AreEqual("Valeur1", EnumHelper.GetLabelForItem(TestEnum.Valeur1));
        }
    
        [Test]
        public void ParseTest()
        {
          Assert.AreEqual(TestEnum.Valeur1, EnumHelper.ParseLabel("Valeur1"));
          Assert.Throws(() => EnumHelper.ParseLabel("1"));
          Assert.Throws(() => EnumHelper.ParseLabel("testquelconque"));
    
          Assert.AreEqual(TestEnum.Valeur1, (EnumHelper.ParseLabel("Valeur1", true)));
          Assert.AreEqual(TestEnum.Valeur1, ( EnumHelper.ParseLabel("Valeur1", false) ));
          Assert.Throws(() => EnumHelper.ParseLabel("valeur1"));
          Assert.AreEqual(TestEnum.Valeur1, ( EnumHelper.ParseLabel("valeur1", true) ));
          Assert.Throws(() => EnumHelper.ParseLabel("valeur1", false));
          Assert.Throws(() => EnumHelper.ParseLabel("testquelconque", true));
        }
    
        [Test]
        public void ParseOrNullTest()
        {
          Assert.AreEqual(TestEnum.Valeur1, EnumHelper.ParseOrNullLabel("Valeur1"));
          Assert.IsNull(EnumHelper.ParseOrNullLabel("1"));
          Assert.IsNull(EnumHelper.ParseOrNullLabel("testquelconque"));
          Assert.IsNull(EnumHelper.ParseOrNullLabel(string.Empty));
          Assert.IsNull(EnumHelper.ParseOrNullLabel(null));
    
          Assert.AreEqual(TestEnum.Valeur1, ( EnumHelper.ParseLabelOrNull("Valeur1", true) ));
          Assert.AreEqual(TestEnum.Valeur1, ( EnumHelper.ParseLabelOrNull("Valeur1", false) ));
          Assert.IsNull(EnumHelper.ParseOrNullLabel("valeur1"));
          Assert.AreEqual(TestEnum.Valeur1, ( EnumHelper.ParseLabelOrNull("valeur1", true) ));
          Assert.IsNull(EnumHelper.ParseLabelOrNull("valeur1", false));
          Assert.IsNull(EnumHelper.ParseLabelOrNull("testquelconque", true));
        }
    
        [Test]
        public void TryParseTest()
        {
          TestEnum testEnum = TestEnum.Valeur2;
          bool convOk = EnumHelper.TryParseLabel("Valeur1", out testEnum);
          Assert.IsTrue(convOk);
          Assert.AreEqual(TestEnum.Valeur1, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("1", out testEnum);
          Assert.IsFalse(convOk);
          Assert.AreEqual(TestEnum.Clear, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("testquelconque", out testEnum);
          Assert.IsFalse(convOk);
          Assert.AreEqual(TestEnum.Clear, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("Valeur1", true, out testEnum);
          Assert.IsTrue(convOk);
          Assert.AreEqual(TestEnum.Valeur1, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("Valeur1", false, out testEnum);
          Assert.IsTrue(convOk);
          Assert.AreEqual(TestEnum.Valeur1, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("valeur1", out testEnum);
          Assert.IsFalse(convOk);
          Assert.AreEqual(TestEnum.Clear, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("valeur1", true, out testEnum);
          Assert.IsTrue(convOk);
          Assert.AreEqual(TestEnum.Valeur1, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("valeur1", false, out testEnum);
          Assert.IsFalse(convOk);
          Assert.AreEqual(TestEnum.Clear, testEnum);
    
          testEnum = TestEnum.Valeur2;
          convOk = EnumHelper.TryParseLabel("testquelconque", true, out testEnum);
          Assert.IsFalse(convOk);
          Assert.AreEqual(TestEnum.Clear, testEnum);
        }
    
        [Test]
        public void GetNamesTest()
        {
          Assert.NotNull(EnumHelper.GetLabels());
          Assert.NotNull(EnumHelper.GetItems());
        }
      }
    }
    

    Hope this would help somebody. Regards Richard

    Richard 5 March 2013