In spite this is a quite old question, I found it interesting, because there are several options to call a method dynamically. Literally, it is reflection, expression trees, and emitter. Historically, reflection is the slowest option, whilst emitter is the fastest one. Therefore, I've decided to compare them in this intriguing case and figure out if there are any changes nowadays. The original question asks for ** the best way to call a generic method when the type parameter isn't known at compile time**. However, almost all answers above suggested to use reflection.
I have created three test cases for all mentioned approaches. First, here is a slightly modified sample class that is going to be tested with 3 methods: TestReflection, TestExpression, and TestEmit.
public class Sample
{
public void TestDirectCall(Type type)
{
GenericMethod<string>();
GenericMethodWithArg<string>(42);
StaticMethod<string>();
StaticMethodWithArg<string>(6);
}
public void TestReflection(Type type)
{
CallViaReflection.CallGenericMethod(this, type);
CallViaReflection.CallGenericMethod(this, type, 42);
CallViaReflection.CallStaticMethod(type);
CallViaReflection.CallStaticMethod(type, 6);
}
public void TestExpression(Type type)
{
CallViaExpression.CallGenericMethod(this, type);
CallViaExpression.CallGenericMethod(this, type, 42);
CallViaExpression.CallStaticMethod(type);
CallViaExpression.CallStaticMethod(type, 6);
}
public void TestEmit(Type type)
{
CallViaEmit.CallGenericMethod(this, type);
CallViaEmit.CallGenericMethod(this, type, 42);
CallViaEmit.CallStaticMethod(type);
CallViaEmit.CallStaticMethod(type, 6);
}
public void T()
{
StaticMethod<string>();
}
public void GenericMethod<T>()
{
}
public void GenericMethodWithArg<T>(int someValue)
{
}
public static void StaticMethod<T>()
{
}
public static void StaticMethodWithArg<T>(int someValue)
{
}
}
The class CallViaReflection represents a helper class that makes calls of generic methods via reflection. I have decided to introduce a cache for better results.
public static class CallViaReflection
{
private readonly static Cache<MethodInfo> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
callDelegate.Invoke(sample, null);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(sample, new object[] { someValue });
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
callDelegate.Invoke(null, null);
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(null, new object[] { someValue });
}
private static MethodInfo GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var concreteMethodInfo))
return concreteMethodInfo;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
cache.Add(methodName, genericType, concreteMethodInfo);
return concreteMethodInfo;
}
}
The next class CallViaExpression uses cached expression trees.
public static class CallViaExpression
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var argumentExpr = arguments.Select((type, i) => Expression.Parameter(type, "arg" + i)).ToArray();
if (concreteMethodInfo.IsStatic)
{
var callExpr = Expression.Call(concreteMethodInfo, argumentExpr);
callDelegate = Expression.Lambda(callExpr, argumentExpr).Compile();
}
else
{
var parameterExpr = Expression.Parameter(sampleType, "sample");
var callExpr = Expression.Call(parameterExpr, concreteMethodInfo, argumentExpr);
callDelegate = Expression.Lambda(callExpr, new[] { parameterExpr }.Union(argumentExpr).ToArray()).Compile();
}
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
}
And the last but not least CallViaEmit emits necessary operations.
public static class CallViaEmit
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(this Sample sample, Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(this Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType);
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDynamicMethod(string methodName, BindingFlags bindingFlags, Type genericType)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var genericMethodInfo = typeof(Sample).GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var argumentTypes = concreteMethodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); ;
var dynamicMethodArgs = concreteMethodInfo.IsStatic
? argumentTypes
: new[] { typeof(Sample) }.Union(argumentTypes).ToArray();
var dynamicMethod = new DynamicMethod("DynamicCall", null, dynamicMethodArgs);
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Nop);
switch (dynamicMethodArgs.Length)
{
case 0:
break;
case 1:
il.Emit(OpCodes.Ldarg_0);
break;
case 2:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
break;
case 3:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
break;
default:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldarg_3);
for (int i = 4; i < argumentTypes.Length; i++)
{
il.Emit(OpCodes.Ldarg, argumentTypes[i]);
}
break;
}
il.EmitCall(concreteMethodInfo.IsStatic ? OpCodes.Call : OpCodes.Callvirt, concreteMethodInfo, null);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret);
callDelegate = dynamicMethod.CreateDelegate(GetActionType(dynamicMethodArgs));
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
private static Type GetActionType(Type[] argumentTypes)
{
switch (argumentTypes.Length)
{
case 0:
return typeof(Action);
case 1:
return typeof(Action<>).MakeGenericType(argumentTypes);
case 2:
return typeof(Action<,>).MakeGenericType(argumentTypes);
case 3:
return typeof(Action<,,>).MakeGenericType(argumentTypes);
case 4:
return typeof(Action<,,,>).MakeGenericType(argumentTypes);
case 5:
return typeof(Action<,,,,>).MakeGenericType(argumentTypes);
case 6:
return typeof(Action<,,,,,>).MakeGenericType(argumentTypes);
case 7:
return typeof(Action<,,,,,,>).MakeGenericType(argumentTypes);
case 8:
return typeof(Action<,,,,,,,>).MakeGenericType(argumentTypes);
default:
throw new NotSupportedException("Action with more than 8 arguments is not supported");
}
}
}
Finally, here is a test class and test results.
[TestFixture]
public class SampleTests
{
private const int Iterations = 10000000;
[Test]
public void TestDirectCall()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestDirectCall(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods directly took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestReflection()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestReflection(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via reflection took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestExpressionTree()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestExpression(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via expression tree took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestEmit()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestEmit(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via emit took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
}
Calling methods dynamically via emit took 2939 milliseconds.
Calling methods dynamically via expression tree took 3910 milliseconds.
Calling methods dynamically via reflection took 6381 milliseconds.
Obviously, the winner is emitter. It still performs more than twice faster. Second place is for expression trees.
Therefore, my verdict has not beeing changed for second decade yet. If you need to call a method dynamically, start going with expression trees. If your code is performance-critical, use ILGenerator and emit necessary calls. In spite, it might look complicated at first glance, all necessary MSIL instruction could be easily disassembled using ildasm.
The source code is available on GitHub.
BindingFlags.Instance
, not justBindingFlags.NonPublic
, to get the private/internal method. – Martinmartina