Wednesday, April 23, 2008

Enum.TryParse with .NET 3.5 extension methods

Among other awesome features introduced in C# 3.0 are amazing extension methods. It looks like taking on board dynamic-language-oriented people like John Lam is starting to pay out for Microsoft. Now this Ruby-style can be done in C#:

42.IsInRange(range);

Woohoo! The feature is quite powerful and if you think about that - a little bit intimidating for a static-language mind. Ruby people will be definitely more comfortable with the idea of such a freedom.
A release earlier - in .NET 2.0 - Microsoft gave us TryParse method, a good way to write cleaner code and improve performance avoiding costly Try-Catch blocks (judging by the messy code it was the last-minute addition, though). Sadly, for some reason the Enum type wasn't equipped with this useful feature. That means it is a good reason to try out some extensions...


The following code is a first attempt, which is more in line with the traditional TryParse syntax:

public static bool TryParse(this Enum theEnum, Type enumType, object value, out Enum result)
{
result = theEnum;
foreach (string item in Enum.GetNames(enumType))
{
if (item.Equals(value))
{
result = (Enum)Enum.Parse(enumType, value.ToString());
return true;
}
}

if (Enum.IsDefined(enumType, Convert.ChangeType(value, (Enum.GetUnderlyingType(enumType)))))
{
result = (Enum)Enum.Parse(enumType, value.ToString());
return true;
}
return false;
}
The usage is obvious:
bool result = type.TryParse(typeof(StatusEnum), "SerialMonogamist", out parsedEnum);
I avoided using a brute force approach with catching parsing errors - it defeats the whole purpose of having TryParse in the first place. The parse attempt is split into string and number parsing. The reason I didn't use the Enum.IsDefined(enumType, value) for strings is it will fail for a numeric values with type different from the enum's underlying type. I assumed that parsing Int16 or Int64 to the Int32-based enum should work flawlessly (mind the Overflow exception).

A little bit different but more elegant code with generics:

public static bool TryParse<T>(this T theEnum, object value, out T result)
{
result = theEnum;
foreach (string item in Enum.GetNames(typeof(T)))
{
if (item.Equals(value))
{
result = (T)Enum.Parse(typeof(T), value.ToString());
return true;
}
}

if (Enum.IsDefined(typeof(T), Convert.ChangeType(value, (Enum.GetUnderlyingType(typeof(T))))))
{
result = (T)Enum.Parse(typeof(T), value.ToString());
return true;
}
return false;
}
The method call looks cleaner (CLR is smart enough to deduce a proper signature even if generic type is omitted) but a bit less readable:
bool result = theEnum.TryParse("HonestHusband", out parsedEnum);
I've put some small test coverage project together and you can use it when trying to perfect my humble code. Good luck! Now it should be less reasons to hate C# generics :)

3 comments:

AnEnglishmanInNorway said...

Thanks a mint for this, it's the best implementation I found. It wasn't immediately clear that I should call it like this:
MyEnum e = default(MyEnum);
bool ok = e.TryParse(enumString, out e);
but maybe that's obvious if you're a heavy duty generics programmer, which I am not. I wondered a lot about the call to
ChangeType. Actually, you should use a try/catch on that, because it will throw an exception when faced with an illegal
string - and one uses TryParse precisely to avoid exceptions.

Cheers, Pete


public static bool TryParse<T>(this T theEnum, object valueToParse, out T result)
{
result = default(T);
foreach (string enumMemberName in Enum.GetNames(typeof(T)))
{
if (enumMemberName.Equals(valueToParse))
{
result = (T)Enum.Parse(typeof(T), valueToParse.ToString());
return true; // the normal case when the valueToParse is good
}
}
try
{
Type underlyingType = Enum.GetUnderlyingType(typeof(T)); // for an enum this is usually an int
object underlyingTypeObject = Convert.ChangeType(valueToParse, underlyingType); // this could throw!
if (Enum.IsDefined(typeof(T), underlyingTypeObject))
{
result = (T)Enum.Parse(typeof(T), valueToParse.ToString());
return true;
}
}
catch (FormatException ex) { ComplainAboutInvalidEnum<T>(valueToParse.ToString(), ex.Message); return false; }
catch (InvalidCastException ex) { ComplainAboutInvalidEnum<T>(valueToParse.ToString(), ex.Message); return false; }
return false;
}

FishOfPrey said...

Very helpful. Thanks. Especially the conversion to the underlying type of the Enum.

I do find it a bit odd that you could pass in long.MaxValue when the underlying type is Int32 and it will throw overflow exceptions (as you point out).

Personally I'm using Enum.IsDefined and Enum.Parse if the value is string or the underlying type and then only doing ChangeType if the value is a string.

Michael Goldobin said...

Pete:
I can't recall exactly all details behind the decisions I made composing this sample but one was for sure - my main motivation was to avoid using Try/Catch alltogether, as this construct is less performing than TryParse. As I pointed out earlier, the usage of Try/Catch block would defeat the whole purpose of the excercise.
FishOfPrey: sure enough, conversions are not that simplle as they appear and I expect them become even trickier with dynamic .NET 4.0 :)


© 2008-2013 Michael Goldobin. All rights reserved