Introduction
Enum
type has few helper methods which make parsing strings into related enum
value easy. There is Parse
, as well as TryParse
. Both can takes an extra parameter to specify whether the operation is case sensitive.
Unfortunately, it is not enough, as both methods will happily parse any natural number which fits underlying type (int
by default):
enum Colour { Red = 1, Green = 2, Blue = 3 }
var s = Colour.TryParse("4", out Colour c)
// s = true, c = 4
To determine whether given value exists in specified enumeration you can use Enum.IsDefined
method:
var a = Enum.IsDefined(typeof(Colour), c)
// a = false
Performance considerations
Turns out above methods, as useful as they are, result in very poor performance. For majority of cases (see note at the end of this article) the performance would be acceptable, but in high-performance code this shows up as a bottleneck.
Fortunately, this can be easily solved through use of dictionary as a map between text and enum
value:
enum Colour { Red = 1, Green = 2, Blue = 3 }
var d = new Dictionary<string, Colour>
{
{ "Red", Colour.Red},
{ "1", Colour.Red},
{ "Green", Colour.Green},
{ "2", Colour.Green},
{ "Blue", Colour.Blue},
{ "3", Colour.Blue}
};
var s = d.TryGetValue("4", out var c)
// s = false, c = 0;
That will perform much better but there are two potential issues:
- code repetition - values are defined twice: first in enum definition and then when filling up dictionary
- case-sensitivity - current solution doesn't cover case-insensitive parsing.
Code repetition
Although it may not seem like much of a problem now, it could lead to a bug when we add new item to the Colour
enum and forget to add it into related dictionary. By using Enum.GetValues()
method we can automatically fill dictionary with relevant values:
Enum
.GetValues(typeof(Colour))
.Cast<Colour>()
.SelectMany(v =>
new[] {
(n: v.ToString().ToLower(), v), // get string representation of value
(n: ((int)v).ToString(), v) // get int representation of value
})
.ToDictionary(i => i.n, i => i.v);
Case insensitive parsing
If required, case-insensitive parsing may be done by providing relevant IEqualityComparer<TKey>
to the constructor of the dictionary:
Enum
.GetValues(typeof(Colour))
.Cast<Colour>()
.SelectMany(v =>
new[] {
(n: v.ToString().ToLower(), v), // get string representation of value
(n: ((int)v).ToString(), v) // get int representation of value
})
.ToDictionary(i => i.n, i => i.v, StringComparer.OrdinalIgnoreCase);
Benchmarking
To verify impact of changes on performance I am using BenchmarkDotNet which is great and easy to use library for benchmarking .NET code.
I set up three test cases:
- Using
TryParse
andIsDefined
- Using
TryParse
only - good in cases where you can be sure values to parse are valid - Using mapping
Dictionary
discussed above
Running it on my laptop with Intel i7 CPU I got following results:
Method | Mean | Error | StdDev |
---|---|---|---|
TryParseAndIsDefined | 549.42 ms | 6.479 ms | 6.060 ms |
TryParseOnly | 313.08 ms | 4.472 ms | 3.965 ms |
UsingDictionary | 71.85 ms | 1.331 ms | 1.245 ms |
As you can see, optimised version with mapping dictionary is over 7 time faster then TryParse
with IsDefined
with much lower error level!
Having that code in a tight-loop which is executed thousands of times a minute can significantly improve performance.
You can find Benchmark code here: https://github.com/mariuszwojcik/Blog-posts-code/blob/master/Benchmarks/EnumParse.cs
Note on performance optimisations
Performance optimisations can lead to code which is less readable and harder to maintain. It's always a good idea to profile the application to see whether optimisation is required. BenchmarkDotNet is a great tool to measure different approaches to solve the problem and how they improve speed and memory management.