18 July 2010

Creating a light-weight visitor, fluently in C#

In object-oriented programming a common problem is performing some conditional logic based on the type of an object at run-time. For example, one form you may come across is:

public void DoStuff(MemberInfo memberInfo)
{
 EventInfo eventInfo = memberInfo as EventInfo;
 if(eventInfo != null)
 {
  // do something
  return;
 }

 MethodInfo methodInfo = memberInfo as MethodInfo;
 if(methodInfo != null)
 {
  // do something
  return;
 }

 PropertyInfo propertyInfo = memberInfo as PropertyInfo;
 if(propertyInfo != null)
 {
  // do something
  return;
 }

 throw new Exception("Not supported.");
}

Drawbacks to this being you have to wrap the whole thing in a method to make use of the "bomb out" return statements and it's quite a lot of code repetition which, as I’ve talked about previously, I'm not a fan of. Another example is a dictionary type->operation lookup:

// set up some type to operation mappings
static readonly Dictionary<Type, Action<MemberInfo>> operations = new Dictionary<Type, Action<MemberInfo>>();

// probably inside the static constructor...
operations.Add(typeof(EventInfo), memberInfo => 
{
 EventInfo eventInfo = (EventInfo)memberInfo;
 // do somthing 
});
operations.Add(typeof(MethodInfo), memberInfo =>
{
 MethodInfo methodInfo = (MethodInfo)memberInfo;
 // do something
});
operations.Add(typeof(PropertyInfo), memberInfo =>
{
 PropertyInfo propertyInfo = (PropertyInfo)memberInfo;
 // do something
});

// use it like this...
Type type = memberInfo.GetType();
Type matchingType = operations.Keys.FirstOrDefault(t => t.IsAssignableFrom(type));
if(matchingType != null)
{
 operations[matchingType](memberInfo);
}

The major drawback with this method is that you have to use IsAssignableFrom otherwise it doesn't match inherited types. In fact, the above example doesn't work if you just look up the type of memberInfo directly because we'll get types derived from EventInfo etc, not those types themselves. We also still need to cast to the type we want to work with ourselves and enumerating the dictionary isn’t ideal from a performance point of view.

The GoF pattern for solving this is the visitor which I’ve blogged about in the past however this is rather heavy duty, especially if your "do something" is only one line. It is much more performant than the alternatives though, as it’s using low level logic inside the run-time to make the decision about which method to call, so that should be a consideration.

Then next best alternative to the proper visitor is the first ...as...if...return... form but we can wrap it up quite nicely with a couple of extension methods to cut down on the amount of code we have to write. Here’s a trivial example trying to retrieve the parameters for either a method or a property. Depending on the type we need to call a different method so we identify that method using a fluent visitor:

private Type[] GetParamTypes(MemberInfo memberInfo)
{
 Func<ParameterInfo[]> paramGetter = null;

 memberInfo
  .As<MethodInfo>(method => paramGetter = method.GetParameters)
  .As<PropertyInfo>(property => paramGetter = property.GetIndexParameters)
  .As<Object>(o => { throw new Exception("Unsupported member type."); });

 return paramGetter().Select(pi => pi.ParameterType).ToArray();
}

The As extension attempts to cast “this” as the type specified by the type parameter T and if successful calls the supplied delegate. The overload used in the example above will skip the remaining As calls once one has been successful. There is a second overload which takes a Func<T, bool> rather than an Action<T> and will continue to try the next As if false is returned from the Func. The last As call, by specifying Object as the type, is a catch all and allows providing a default implementation or catering for an error case as shown above. The extensions are implemented like so:

/// <summary>
/// Tries to cast an object as type <typeparamref name="T"/> and if successful 
/// calls <paramref name="operation"/>, passing it in.
/// </summary>
/// <typeparam name="T">Type to attempt to cast <paramref name="o"/> as</typeparam>
/// <param name="o"></param>
/// <param name="operation">Operation to be performed if cast is successful</param>
/// <returns>
/// Null if the object cast was successful, 
/// otherwise returns the object for chaining purposes.
/// </returns>
public static object As<T>(this object o, Action<T> operation)
 where T : class
{
 return o.As<T>(obj => { operation(obj); return true; });
}

/// <summary>
/// Tries to cast an object as type <typeparamref name="T"/> and if successful 
/// calls <paramref name="operation"/>, passing it in.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="o"></param>
/// <param name="operation">Operation to be performed if cast is successful, must return 
/// a boolean indicating whether the object was handled.</param>
/// <returns>
/// Null if the object cast was successful and <paramref name="operation"/> returned true, 
/// otherwise returns the object for chaining purposes.
/// </returns>
public static object As<T>(this object o, Func<T, bool> operation)
 where T : class
{
 if (Object.ReferenceEquals(o, null)) return null;

 T t = o as T;
 if (!Object.ReferenceEquals(t, null))
 {
  if (operation(t)) return null;
 }
 return o;
}

No comments: