Previously I discussed how to work around the variance limitations of the Delegate.CreateDelegate method. CreateDelegate has quite a few overloads, and acts differently based on the parameters which are passed. It also allows you to create open instance or closed static delegates. Without going into too much detail:
- Closed delegate: an instance is bound to the delegate along with the method. In practice this means when calling the delegate, it will always operate on the same instance.
- Open delegate: not bound to a specific target object until invocation time. An instance on which the method should operate is passed as first argument during invocation.
For now, I will only discuss creating open instance delegates. These allow you to invoke an instance method, while passing a first argument, specifying the instance on which it should be invoked.
Divide your programs into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction. – Kent Beck’s Smalltalk Best Practice Patterns
In an attempt to better follow this principle, better known as the Single Level of Abstraction (SLAP) principle, I’ve split up the CreateDelegate method into two helper methods. One to create ‘simple’ delegates, and one to create open instance delegates. Later, an additional third method could be added to create closed static delegates.
By ‘simple’ delegates I mean delegates of which the signature matches that of MethodInfo. More particularly, the method has the same amount of arguments than the delegate.
public static TDelegate CreateDelegate<TDelegate>( MethodInfo method, object instance = null, CreateOptions options = CreateOptions.None ) where TDelegate : class { ... }
By default, this method results in a simple CreateDelegate call. Instead of having to pass the delegate type as an argument a generic approach is used, eliminating the need to cast the returned delegate. When the options argument is set to CreateOptions.Downcasting, this method behaves like the CreateDowncastingDelegate method from my previous post. Downcasts from the delegate argument types and return type to the required types of the method are generated where needed.
The helper method to create open instance delegates looks as follows:
/// <summary> /// Creates a delegate of a specified type that represents a method /// which can be executed on an instance passed as parameter. /// </summary> /// <typeparam name = "TDelegate"> /// The type for the delegate. This delegate needs at least one (first) type parameter /// denoting the type of the instance which will be passed. /// E.g. Action<ExampleObject, object>, /// where ExampleObject denotes the instance type and object denotes /// the desired type of the first parameter of the method. /// </typeparam> /// <param name = "method">The MethodInfo describing the method of the instance type.</param> /// <param name = "options">Options which specify what type of delegate should be created.</param> public static TDelegate CreateOpenInstanceDelegate<TDelegate>( MethodInfo method, CreateOptions options = CreateOptions.None ) where TDelegate : class { ... }
The comments indicate clearly what TDelegate should look like. Of course, no instance can be passed, clearly specifying it needs to be specified in the delegate type. Furthermore, the passed MethodInfo is required to be an instance method. All these measures are meant to clarify this specific usage of CreateDelegate. Additionally, similar to passing CreateOptions.Downcasting to the other helper method, it allows you to break the variance safety of the ordinary CreateDelegate method.
Both helper functions can be found in DelegateHelper.cs. I also added MethodInfo extension functions to the reflection extensions, resulting in a more concise syntax.
Example usage:
MethodInfo toUpperMethod = typeof( string ).GetMethod( "ToUpper", new Type[] { } ); Func<string, string> toUpper = toUpperMethod.CreateOpenInstanceDelegate<Func<string, string>>(); string upper = toUpper( "test" ); // Will result in "TEST".