One of the benefits of generics is that it doesn't have to matter what type you're working with as long as it meets the criteria of the operation.
So what if you're parsing input in a generic method and you need to be able to do a TryParse-like operation on any type for which the method is called? With a type converter, a very small number of types is supported, mostly scalar types. Using a little reflection, the code in this article makes it possible to work with any type for which a TryParse method is available.
The first time I went down this road, I fell into it somewhat by accident. Fortunately, I realized that I didn't need it and was able to abandon the mess. The second time, I was aware that I was about to start down that road again and it was clear that it was necessary. The result is a flexible method with a granular infrastructure.
Note: I use the term "parser" to refer to a TryParse method for a specific type, thus avoiding confusion with the TryParse methods being developed here.
Prerequisites
I want to start by stating the design goals of this development effort:
- To support any type as long as a parser with the required signature is available.
- To defer to the type whenever possible.
- To allow the consumer to provide parsing functionality for types they can't modify.
- To prevent overriding type-provided functionality (if different behavior is used, it should be apparent at the call site).
- To minimize reflection.
There are actually two methods being developed here; one is a type-safe, generic method, TryParse<T>, and the other is the non-generic version, TryParse, which can be useful in its own right when code is dealing with a Type instance instead of a generic type parameter.
The first thing to do is formalize the required method signature I mentioned in the design goals:
static bool TryParse(string s, out T result);
The method must be static, it must return a bool, it must be named "TryParse", it must take a string as its first parameter, and it must take a reference to the type it parses as its second parameter. What can't be readily concluded from this signature (because it's shown here as T) is that it must not have any unresolved generic parameters. In CLR terms, it must be a closed constructed method. We'll need to test all these aspects before we can use a parser.
HasTryParseSignature encapsulates that logic. One thing to point out is that the parser only has to be public in a certain situation. I'll explain why later.
Discovery
In order to prevent multiple lookups of the same method, we need to keep a dictionary that maps Type to MethodInfo. If no parser is found, we still store null to prevent duplicate searches, regardless of success or failure.
GetTryParseMethod returns a parser from the cache, if available. Otherwise, it attempts to find a parser and cache it, then return the result. Finding an appropriate parser implemented by a type is fairly simple:
BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static;
Type[] parameterTypes = new Type[] { typeof(string), type.MakeByRefType() };
MethodInfo method = type.GetMethod("TryParse", bindingFlags, null, parameterTypes, null);
The key is the call to type.MakeByTypeRef(); just using type won't work.
FindTryParseMethod does the above reflection and returns the result or null. The only thing that the above code doesn't do is guarantee that the method's return type is bool so FindTryParseMethod tests that separately.
The above code uses BindingFlags.Public to find a public TryParse method on the type. This is because we need to be sure that the type's author intended it to be used by consumers. But when providing a consumer-implemented parser, visibility doesn't matter.
SetTryParseMethod allows the consumer to provide a parser for a type over which the consumer has no control. For example, Guid doesn't expose a parser, but it wouldn't be difficult to provide one (the attached code automatically provides it).
SetTryParseMethod ensures that a type-provided parser is unavailable (see design goal #4), confirms that the consumer-provided parser has the correct signature, and then associates it with the type it parses in the cache. There are two versions available, one that takes a MethodInfo instance, and one that takes an instance of the following delegate type:
public delegate bool TryParseDelegate<T>(string s, out T result);
Putting it all together
Finally, we need to be able to call a TryParse method on any type. Earlier I mentioned that two TryParse methods are being implemented here. We'll start with the non-generic version because it's the workhorse of the two.
public static bool TryParse(this string s, Type type, out object result)
{
result = null;
MethodInfo method = GetTryParseMethod(type);
if (method == null)
throw new Exception(...);
object[] parameters = new object[] { s, null };
bool success = (bool)method.Invoke(null, parameters);
if (success)
result = parameters[1];
return success;
}
This allows us to use any type, specified as a parameter. The result is an object, but the value being assigned upon success is of the specified type (by virtue of the fact that we validated the method signature and associated it with the type).
Now, implementing the generic version is rather trivial:
public static bool TryParse<T>(this string s, out T result)
{
result = default(T);
object tempResult;
bool success = TryParse(s, typeof(T), out tempResult);
if (success)
result = (T)tempResult;
return success;
}
In either case, we don't check s because a type may have a special meaning for a null string. It's best to let the TryParse method determine what to do with it.
Final notes
An exception is thrown by TryParse to indicate that no parser is available for a type. This kind of exception will most likely be detected during development and testing. If you wanted to return false instead, you could do it but you would run the risk of failures slipping under the radar just because someone forgot to implement a parser.
Finally, I implemented these TryParse methods as extension methods on string because:
- Having to qualify the TryParse methods with the class name gets cumbersome.
- Any string could potentially be parsed as some other data type, so it makes sense to see extension methods in Intellisense.
Be sure to check out parts 2, 3, and 4.
Attachment: GenericParsing.zip