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 with 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 with Enum<T>

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<MyEnumbers>.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 to Enums – Better syntax, improved performance and TryParse in NET 3.5

  1. Avatar for

    Information is only used to show your comment. See my Privacy Policy.

  2. Avatar for Richard
    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

  3. Avatar for Richard
    Richard

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

  4. Avatar for Tom Groves
    Tom Groves

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

  5. Avatar for WaSaMaSa
    WaSaMaSa

    Oh, I see.

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

  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<T>...

  7. Avatar for WaSaMaSa
    WaSaMaSa

    Useful class, but instead of Enum<T> use Enum.Method<T> 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<MyEnumbers>.IsDefined(MyEnumbers.Eight);
    var getName = Enum<MyEnumbers>.GetName(MyEnumbers.Eight);
    
    //method type parameter(without auto recognized)
    var isDefined = Enum.IsDefined<MyEnumbers>(MyEnumbers.Eight);
    var getName = Enum.GetName<MyEnumbers>(MyEnumbers.Eight);
    
    //method type parameter(auto recognized)
    var isDefined = Enum.IsDefined(MyEnumbers.Eight);
    var getName = Enum.GetName(MyEnumbers.Eight);
    

    Cheers.

  8. 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

  9. Avatar for Damien Guard

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

    But yes, that’s pretty much it :)

  10. 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.

  11. Avatar for Ryan

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