This is the fourteenth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
We know that when we call virtual method we get runtime polymorphism, dynamic dispatch. Can we somehow call a method method without resolving overriding hierarchy? Or even better, by specifying which override to use?
When we call instance method, internally callvirt
instruction is used. That instruction performs null check and calls method using dynamic dispatch. However, there is another instruction, used for calling methods when no dynamic dispatch is needed. It’s called call
. We can generate some code and use this instruction to call the method directly. See this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
using System; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Linq.Expressions; public class Program { public static void Main() { Base b = new Derived2(); InvokeMethod<Derived>(() => b.Foo(123 + 456)); } public static void InvokeMethod<T>(Expression<Action> caller) where T : class { if (caller.Body.NodeType != ExpressionType.Call) { throw new ArgumentException("Parameter must call a method", "caller"); } var castedExpression = (MethodCallExpression)caller.Body; var evaluatedArguments = castedExpression.Arguments.Select(a => GetLambda(a).Invoke()).ToArray(); var methodToCall = typeof(T).GetMethod(castedExpression.Method.Name, evaluatedArguments.Select(a => a.GetType()).ToArray()); var methodInvoker = GetCaller<T>(castedExpression, methodToCall); methodInvoker.Invoke((T)GetLambda(castedExpression.Object).Invoke(), evaluatedArguments); } public static Action<T, object[]> GetCaller<T>(MethodCallExpression expression, MethodInfo methodToCall){ var helperMethod = new DynamicMethod(string.Empty, typeof(void), new[] { typeof(T), typeof(object[]) }, typeof(T).Module, true); var ilGenerator = helperMethod.GetILGenerator(); ilGenerator.Emit(OpCodes.Ldarg_0); for (int i = 0; i < expression.Arguments.Count(); ++i) { var argumentType = expression.Arguments[i].Type; ilGenerator.Emit(OpCodes.Ldarg_1); ilGenerator.Emit(OpCodes.Ldc_I4, i); ilGenerator.Emit(OpCodes.Ldelem, typeof(object)); if (argumentType.IsValueType) { ilGenerator.Emit(OpCodes.Unbox_Any, argumentType); } } ilGenerator.Emit(OpCodes.Call, methodToCall); ilGenerator.Emit(OpCodes.Ret); var methodInvoker = (Action<T, object[]>)helperMethod.CreateDelegate(typeof(Action< T, object[]>)); return methodInvoker; } public static Func<object> GetLambda(Expression a) { return Expression.Lambda<Func<object>>(Expression.Block(Expression.Convert(a, typeof(object)))).Compile(); } } public class Base{ public virtual void Foo(int dummyParameter){ Console.WriteLine("Base: " + dummyParameter); } } public class Derived : Base{ public override void Foo(int dummyParameter){ Console.WriteLine("Derived: " + dummyParameter); } } public class Derived2 : Derived{ public override void Foo(int dummyParameter){ Console.WriteLine("Derived2 " + dummyParameter); } } |
We have three classes in our hierarchy. We create an instance of the Derived2
, assign it to a variable of type Base
and we would like to call the method “in the middle”, from Derived
.
We wrap the method using lambda. That way we can explain which method we would like to call (and which parameters to use) but we don’t actually call it. We also use generic type parameter to specify a class with implementation of the method we would like to use.
First, we check if lambda is of correct form (line 17). Next, we evaluate arguments (line 22) and find correct method to call using the reflection (line 23). Then, we just create a generic instance of the helper method and create the lambda.
We load the instance (line 35), next, we load all arguments (and unbox them if needed) (lines 36-46). Finally, we call the method and return from the function. Ultimately, we just need to create the lambda (line 49) and return it.
Then, we call the method in line 25 and we are done. Output:
1 |
Derived: 579 |
Notice, that it works correctly even if we remove override from Derived
class.