Want to show your appreciation? Please to my charity.

Monday, May 18, 2009

Strong Typed, High Performance Reflection with C# Delegate (Part III)

Update: Open source project SharpCut delivers a less than 50K library which does what described in this series plus much more. Check it out.

Content

  1. Inspiration: C#.Net Calling Grandparent's Virtual Method (base.base in C#)
  2. Prototype: Strong Typed, High Performance Reflection with C# Delegate (Part I)
  3. Performance: Strong Typed, High Performance Reflection with C# Delegate (Part II)
  4. Library Usage: Strong Typed, High Performance Reflection with C# Delegate (Part III) <= you are here

In this post, we are going to discuss how you can easily get a Delegate that let you make high performance reflection call by using various extension methods in a library named CommonReflection.

You can download CommonReflection's binary distribution here. Source code can be checked out from Subversion repository  http://kennethxublogsource.googlecode.com/svn/trunk/CommonReflection hosted on Google Code.

There are ten extension methods defined all in one class of the CommonReflection to let you easily obtain a Delegate to any method by name from a type or an instance of object. Amount those, fix (6) extends System.Type and four (4) extends object type. We'll cover all of them in the following sections.

type.GetStaticInvoker<TDelegate>(string staticMethodName)

This extension method finds a static method with the given name regardless of the scope (i.e. it gets private method too) that

  1. The method has the same number of parameters as TDelegate
  2. Each method parameter must be assignable FROM the corresponding parameter of the Delegate at the same position
  3. The method return type must be assignable TO the return type of the Delegate.
  4. For out and ref parameters, they must match exactly.

In a nutshell, you need to make sure you can make the method call with parameters of type of TDelegate and the return result must be of a sub type of TDelegate's return type.

Giving an example, if you have a method and Delegate defined as

        class MyClass {
            private static Sub Foo(Base b, int i, object o) { return null; }
        }

        private delegate Sub ExactFoo(Base b, int i, object o);
        private delegate Base MatchFoo(Sub b, int i, string s);
        private delegate Sub DoNotMatchFoo(Base b, short i, string s);

Both calls below will return a valid Delegate to invoke the method Foo.

            typeof(MyClass).GetStaticInvoker<ExactFoo>("Foo"); // Good
            typeof(MyClass).GetStaticInvoker<MatchFoo>("Foo"); // Good

But this one will get you a null because "short" is not a sub type of "int".

            typeof(MyClass).GetStaticInvoker<DoNotMatchFoo>("Foo"); // Returns null

type.GetInstanceInvoker<TDelegate>(string instanceMethodName)

Similar to the type.GetStaticInvoker, type.GetInstanceInvoker extension method returns a Delegate that can be used to invoke an instance method on a given type. Because the method is obtained from a Type object, it is not associated to a specific instance. Thus, the instance need to be passed as the first parameter to the Delegate when it is called.

The method matching rules are similar to its static brother except that the first parameter of the TDelegate matches the type object and the second parameter of TDelegate matches to the first parameter of method and so on.

  1. The method has the exactly one less parameters than what TDelegate has.
  2. The first parameter of TDelegate must be assignable TO the given type passed to the extension method.
  3. Each method parameter must be assignable FROM the corresponding parameter of the Delegate at the same position plus one (1). i.e. First parameter of the method matches to the second parameter of Delegate and so on.
  4. The method return type must be assignable TO the return type of the Delegate.
  5. For out and ref parameters, they must match exactly.

Given below class and delegate definition.

        class Parent { private Sub Bar(int i, object o) { return null; } }
        class Child : Parent { }

        private delegate Sub ExactBar(Parent instance, int i, object o);
        private delegate Base MatchBar(Child instance, int i, string s);
        private delegate Base DoNotMatchBar(object instance, int i, object s);

Delegate ExactBar and MatchBar are good match for the Bar instance method and DoNotMatchBar won't match because first parameter is not assignable to Parent type.

            typeof(Parent).GetInstanceInvoker<ExactBar>("Bar"); // Good match
            typeof(Parent).GetInstanceInvoker<MatchBar>("Bar"); // Good match
            typeof(Parent).GetInstanceInvoker<DoNotMatchBar>("Bar"); // Returns null

And here is an example of use

            static MatchBar Bar = typeof(Parent).GetInstanceInvoker<MatchBar>("Bar");

            void AnyMember(Child instance) {
                // High performance, type safe call to private method of Parent
                Base b = Bar(instance, 12, "testing");
            }

type.GetNonVirtualInvoker<TDelegate>(string virtualMethodName)

type.GetNonVirtualInvoker works very similar as the type.GetInstanceInvoker extension method. They have exactly the same method matching rules. The only difference is when the method is a virtual method, the Delegate returned by type.GetInstanceInvoker behaves the same as the virtual method itself (i.e. when it is overridden, the overriding method is used), but the Delegate returned by type.GetNonVirtualInvoker always call the method it bound to. A good example is to call the virtual method of grandparent, which inspired the development of CommonReflection.

instance.GetInstanceInvoker<TDelegate>(string instanceMethodName)

While the Delegate returned from type.GetInstanceInvoker can be used to invoke on any instances of the given type, instance.GetInstranceInvoker is bound to the specific instance. Unalike type.GetInstanceInvoker, the parameters of TDelegate for instance.GetInstanceInvoker should match the instance method. There is no special first parameter requirement any more so code reads more natural this way. The method matching rules is same as type.GetStaticInvoker and repeated below.

  1. The method has the same number of parameters as TDelegate
  2. Each method parameter must be assignable FROM the corresponding parameter of the Delegate at the same position.
  3. The method return type must be assignable TO the return type of the Delegate.
  4. For out and ref parameters, they must match exactly.

Taking the "Bar" example again, notice that the first special parameter is removed from "ExactBar" Delegate.

        class MyClass { private Sub Bar(int i, object o) { return null; } }

        private delegate Sub ExactBar(int i, object o);

And a use case.

        class MyClassUser {
            private readonly ExactBar Bar;

            public MyClassUser(MyClass myClass) {
                Bar = myClass.GetInstanceInvoker<ExactBar>("Bar");
            }
            
            void AnyMember() {
                var sub = Bar(12, "object");
            }
        }

CAUTION: Bare in mind that the benefit of high performance is from the reuse of the Delegate. If we had to generate the Delegate again and again for each call, we may end up with more overhead then simple reflection. Obviously, the reusability of instance.GetInstanceInvoker is reduced comparing to type.GetInstanceInvoker because the former strongly bound to one instance while the later can be used for different instances.

instance.GetNonVirtualInvoker<TDelegate>(Type type, string virtualMethodName)

Like instance.GetInstanceInvoker, instance.GetNonVirtualInvoker provides the similar functionality as type.GetNonVirtualInvoker except that it returns a Delegate that is bound to the given instance. It is obvious that the method matching rules are same as instance.GetInstanceInvoker.

Please note that this extension method takes one more argument then others --the argument "type". Unlike instance.GetInstanceInvoker, which can infer the type from the instance, instance.GetNonVirtualInvoker needs to be told about the type to lookup the method. It only make sense that you want to get a non-virtual invoker to a method defined in the ancestor of the given instance. It also implies that the runtime type of the instance must be assignable to the given type.

CAUTION: Same caution about the reusability for instance.GetInstanceInvoker applies.

Get???InvokerOrFail<TDelegate>(...)

By now, we have discussed half of the ten extension methods that we mentioned in the beginning. The other half are just minor variations of what have discussed by suffixing the method name with "OrFail". Below listed all the five get or fail extension methods.

    type.GetInstanceInvokerOrFail<TDelegate>("StaticMethodName");
    type.GetInstanceInvokerOrFail<TDelegate>("InstanceMthodName");
    type.GetNonVirtualInvokerOrFail<TDelegate>("VirtualMethodName");
    instance.GetInstanceInvokerOrFail<TDelegate>("InstanceMethodName");
    instance.GetNonVirtualInvokerOrFail<TDelegate>(type, "VirtualMethodName");

Those get or fail methods differ from their counterpart by throwing an exception when there is no matching method found. Taking the example in type.GetInstanceInvoker, statement below throws NoMatchException instead of returning a null.

      typeof(Parent).GetInstanceInvokerOrFail<DoNotMatchBar>("Bar"); // exception thrown

Properties and Constructors

As of now, properties and constructors are not supported but can be added in future. Stay tuned at CommonReflection. Hey, it is open source, so you can contribute too!

No comments:

Post a Comment