Monday, 13 September 2010

More C# generic extension methods - non-interface interface methods

(This blog post is copied from my old blog at www.lordzoltan.org, it was originally written 1st August 2008)

Somebody asked me the other day if there was a generic way of providing a property-based clone operation for classes.  The operation didn't have to recurse into complex objects (just copy the references), but the value types had to be copied.

The first part of the solution was a little bit of reflection:

  1. public static void Clone(object source, object dest)
  2. {
  3.     //(omitted) check that the source and destination types
  4.     //are equal with Type.IsAssignableFrom
  5.  
  6.     PropertyInfo[] props = source.GetType().GetProperties(
  7.         BindingFlags.Public |
  8.         BindingFlags.Instance |
  9.         BindingFlags.FlattenHierarchy);
  10.     MethodInfo getmethod;
  11.     MethodInfo setmethod;
  12.     foreach (var p in props)
  13.     {
  14.         //make sure the get and set accessors are available.
  15.         getmethod = p.GetGetMethod();
  16.         setmethod = p.GetSetMethod();
  17.         if (getmethod == null || setmethod == null)
  18.             continue;
  19.         //invoke the getmethod on the source, and use the result to pass
  20.         //to the set method on the destination
  21.         setmethod.Invoke(dest,
  22.             new object[] { getmethod.Invoke(source, new object[] { }) });
  23.     }
  24. }

All great, so now we have a static method we can call - but, however nice it is, it's not that gawk-able.

I wanted to have this method appear in the member list of objects that we wanted to be able to clone - which meant either a base or an interface.  Interfaces are less intrusive, because you can still apply them to classes with bases, of course, and you can have multiple interfaces - but having the method as an interface member didn't make any sense.  However, if you've read my previous post: http://lordzoltan.blogspot.com/2010/09/pseudo-template-meta-programming-in-c_10.html, which uses some generic extensions alongside static generics to provide meta info, the solution becomes easy.  However you have to do something that a lot of people would seriously moan about...  an empty interface.

  1. public interface IGenericCloneable
  2. {
  3. }
  4.  
  5. public static class IGenericCloneableExtensions
  6. {
  7.     public static void Clone<T>(this T source, T dest)
  8.         where T : IGenericCloneable
  9.     {
  10.         PropertyInfo[] props =
  11.             typeof(T).GetProperties(
  12.             BindingFlags.Public |
  13.             BindingFlags.Instance |
  14.             BindingFlags.FlattenHierarchy);
  15.         MethodInfo getmethod;
  16.         MethodInfo setmethod;
  17.         foreach (var p in props)
  18.         {
  19.             getmethod = p.GetGetMethod();
  20.             setmethod = p.GetSetMethod();
  21.             if (getmethod == null || setmethod == null)
  22.                 continue;
  23.             setmethod.Invoke(dest,
  24.                 new object[] { getmethod.Invoke(source, new object[] { }) });
  25.         }
  26.     }
  27. }

Notice that the reflection is performed on typeof(T) instead of source.GetType() - which should be faster at runtime.

There is a gotcha here, though, and it's to do with the T that is passed when this method called for base classes of a superclass, consider the following:

  1. public class Cloneable1 : IGenericCloneable
  2. {
  3.     public int ID { get; set; }
  4. }
  5.  
  6. public class Cloneable2 : Cloneable1
  7. {
  8.     public string Name { get; set; }
  9. }

Now some method declared in another static class, no attempt made to contrive a complicated scenario, instead we just have a rather pointless method, but it demonstrates the issue at hand:

  1. public static class Utility
  2. {
  3.     public static void GetClone(Cloneable1 source,
  4.         Cloneable1 dest)
  5.     {
  6.         source.Clone(dest);
  7.     }
  8. }

Now let's test the method for an instance of Cloneable1 and for Cloneable2:

  1. [TestMethod]
  2. public void TestClone1()
  3. {
  4.     Cloneable1 c1 = new Cloneable1() { ID = 10 };
  5.     Cloneable1 c2 = new Cloneable1();
  6.     Utility.GetClone(c1, c2);
  7.     Assert.AreEqual(c1.ID, c2.ID); //should be fine.
  8. }
  9.  
  10. [TestMethod]
  11. public void TestClone2()
  12. {
  13.     Cloneable2 c1 = new Cloneable2() { ID = 10, Name = "Fred" };
  14.     Cloneable2 c2 = new Cloneable2();
  15.     Utility.GetClone(c1, c2);
  16.     Assert.AreEqual(c1.ID, c2.ID); //should be fine.
  17.     Assert.AreEqual(c1.Name, c2.Name); //will fail
  18. }

The second test will fail, because the GetClone method implicitly passes Cloneable1 as the <T> parameter for the Clone<T> extension method, since T is taken from the left-hand side of the '.' operator which selects the method.

One way to get around this problem is to write another extension method for IGenericCloneable in a new IGenericCloneableExtensions2 class (simplifies the reflection), this time which takes IGenericCloneable and is not generic, we can give it the same name because the compiler will intelligently select the method that matches the MOST derived version of the input parameters when it is called:

  1. public static class IGenericCloneableExtensions2
  2. {
  3.     public static void Clone(this IGenericCloneable source, IGenericCloneable dest)
  4.     {
  5.         typeof(IGenericCloneableExtensions).
  6.             GetMethod("Clone", System.Reflection.BindingFlags.Static
  7.                 | System.Reflection.BindingFlags.Public).MakeGenericMethod(
  8.                     new Type[] { source.GetType() }).Invoke(
  9.                         null, new object[] { source, dest });
  10.     }
  11. }
Now we change the signature of our utility method to take instances of IGenericCloneable instead of the Cloneable1 class, and the compiler will redirect calls to the intermediate method instead of the generic.

This is a useful practise anyway, because it means you can make the IGenericCloneable(2) extensions available to code that only has instances of the interface (which, after all, is what most people will expect of an interface).  However, we can bypass the need to regress the Utility.GetClone method to using IGenericCloneable instance, by tightening up our original generic version with a little bit of reflection and dynamic generic compilation magic:

  1. //modify the original version to check the
  2. //actual instance types of source and dest
  3. public static void Clone<T>
  4.     (this T source, T dest)
  5.     where T : IGenericCloneable
  6. {
  7.     Type[] instancetypes = new Type[] {
  8.         source.GetType(), dest.GetType() };
  9.     
  10.     if (instancetypes[0].Equals(typeof(T)) == false)
  11.     {
  12.         //check ultimate
  13.         if (instancetypes[0].Equals(instancetypes[1])
  14.                 == false &&
  15.              instancetypes[0].IsSubclassOf(instancetypes[1])
  16.              == false)
  17.             throw new ArgumentException("If the two instance types are not equal, " +
  18.                              "the first instance type must be derived from the target type");
  19.         
  20.         ((MethodInfo)MethodInfo.GetCurrentMethod()).
  21.                 GetGenericMethodDefinition().
  22.                      MakeGenericMethod(new Type[] { instancetypes[0] }).
  23.                             Invoke(null, new object[] { source, dest });
  24.         return;
  25.     }
  26.  
  27.     //the rest is the same as before
  28.     PropertyInfo[] props =
  29.         typeof(T).GetProperties(
  30.         BindingFlags.Public |
  31.         BindingFlags.Instance |
  32.         BindingFlags.FlattenHierarchy);
  33.  
  34.     MethodInfo getmethod;
  35.     MethodInfo setmethod;
  36.     foreach (var p in props)
  37.     {
  38.         getmethod = p.GetGetMethod();
  39.         setmethod = p.GetSetMethod();
  40.         if (getmethod == null || setmethod == null)
  41.             continue;
  42.         setmethod.Invoke(dest,
  43.             new object[] { getmethod.Invoke(source, new object[] { }) });
  44.     }
  45. }

Now, we can return our original GetClone method to take instances of Cloneable1, and the two tests should pass.

Don't be worried about the MakeGenericMethod call - the type engine will only actually compile the method once - after that, it will simply go and obtain a handle to the compiled method.  [Note – with the advent of expression trees I now do things like this by dynamically compiling methods and then caching them – the result is far faster and much tidier :)]

There are other things we could do here, for example make another version of Clone which takes two type parameters:

  1. public static void Clone<T1, T2>(this T1 source, T2 dest)
  2.     where T1 : T2
  3.     where T2 : IGenericCloneable
  4. {
  5.     //same code as the original Generic one we wrote,
  6.     //using the property list of T2 instead of T1.
  7. }

This method automatically allows us to clone an instance of T1 to T2 because T1 is inherited from T2, and it'll still appear as an extension method to the instance of T1 because T2 is constrained to implement the IGenericCloneablee interface.

We could then modify the reflection code at the head of the modified generic Clone<T> method so that it picks up this generic method when the instance type of the destination object is a base class of the instance type of the source.

We could write a CloneNew<T> method, by specifying an additional constraint on <T>:

  1. public static T CloneNew<T>(this T source)
  2.     where T : IGenericCloneable, new()
  3. {
  4.     T result = new T();
  5.     source.Clone<T>(result);
  6.     return result;
  7. }

And if you started stacking these empty interfaces on top of others, you can start building whole libraries of useful generic extension methods that provide a whole host of common functionality to otherwise method-less types - and still be able to inherit those types from concrete classes (because the interface list is unbounded).

Have fun!

No comments:

Post a Comment