.Net 的反射是个很好很强大的东西,不过它的效率却实在是不给力。已经有很多人针对这个问题讨论过了,包括各种各样的 DynamicMethod 和各种各样的效率测试,不过总的来说解决方案就是利用 Expression Tree、Delegate.CreateDelegate 或者 Emit 构造出反射操作对应的委托,从而实现加速反射的目的。
虽然本篇文章同样是讨论利用委托来加速反射调用函数,不过重点并不在于如何提升调用速度,而是如何更加智能的构造出反射的委托,并最终完成一个方便易用的委托创建器 DelegateBuilder。
它的设计目标是:
- 能够对方法调用、构造函数调用,获取或设置属性和获取或设置字段提供支持。
- 能够构造出特定的委托类型,而不仅限于 Func<object, object[], object> 或者其它的 Func 和 Action,因为我个人很喜欢强类型的委托,同时类似 void MyDeleagte(params int[] args) 这样的委托有时候也是很有必要的,如果需要支持 ref 和 out 参数,就必须使用自定义的委托类型了。
- 能够支持泛型方法,因为利用反射选择泛型方法是件很纠结的事(除非没有同名方法),而且还需要再 MakeGenericMethod。
- 能够支持类型的显式转换,在对某些 private 类的实例方法构造委托时,实例本身就必须使用 object 传入才可以。
其中的 3、4 点,在前几篇随笔《C# 判断类型间能否隐式或强制类型转换》和《C# 泛型方法的类型推断》中已经被解决了,并且整合到了 PowerBinder 中,这里只要解决 1、2 点就可以了,这篇随笔就是来讨论如何根据反射来构造出相应的委托。
就目前完成的效果,DelegateBuilder 可以使用起来还是非常方便的,下面给出一些示例:
class Program { public delegate void MyDelegate(params int[] args); public static void TestMethod(int value) { } public void TestMethod(uint value) { } public static void TestMethod<T>(params T[] arg) { } static void Main(string[] args) { Type type = typeof(Program); Action<int> m1 = type.CreateDelegate<Action<int>>("TestMethod"); m1(10); Program p = new Program(); Action<Program, uint> m2 = type.CreateDelegate<Action<Program, uint>>("TestMethod"); m2(p, 10); Action<object, uint> m3 = type.CreateDelegate<Action<object, uint>>("TestMethod"); m3(p, 10); Action<uint> m4 = type.CreateDelegate<Action<uint>>("TestMethod", p); m4(10); MyDelegate m5 = type.CreateDelegate<MyDelegate>("TestMethod"); m5(0, 1, 2); } }
可以说效果还是不错的,这里的 CreateDelegate 的用法与 Delegate.CreateDelegate 完全相同,功能却大大丰富,几乎可以只依靠 delegate type、type 和 memberName 构造出任何需要的委托,省去了自己反射获取类型成员的过程。
这里特别要强调一点:这个类用起来很简单,但是简单的背后是实现的复杂,所以各种没有发现的 bug 和推断错误是很正常的。
我再补充一点:虽然在这里我并不打算讨论效率问题,但的确有不少朋友对效率问题有点纠结,我就来详细解释下这个问题。
第一个问题:为什么要用委托来代替反射。如果手头有 Reflector 之类的反编译软件,可以看看 System.Reflection.RuntimeMethodInfo.Invoke 方法的实现,它首先需要检查参数(检查默认参数、类型转换之类的),然后检查各种 Flags,然后再调用 UnsafeInvokeInternal 完成真正的调用过程,显然比直接调用方法要慢上不少。而如果利用 Expression Tree 之类的方法构造出了委托,它就相当于只多了一层方法调用,性能不会损失多少(据说如果 Emit 用得好还能更快),因此才需要利用委托来代替反射。
第二个问题:什么时候适合用委托来代替反射。现在假设有一家公园,它的门票是 1 元,它还有一种终身票,票价是 20 元。如果我只是想进去看看,很可能以后就不再去了,那么我直接花 1 元进去是最合适的。但如果我想天天去溜达溜达,那么花 20 元买个终身票一定更加合适。
相对应的,1 元的门票就是反射,20 元的终身票就是委托——如果某个方法我只是偶尔调用一下,那么直接用反射就好了,反正损失也不是很大;如果我需要经常调用,花点时间构造个委托出来则是更好的选择,虽然构造委托这个过程比较慢,但它受用终身的。
第三个问题:怎么测试委托和反射的效率。测试效率的前提就是假设某个方法是需要被经常调用的,否则压根没必要使用委托。那么,基本的结构如下所示:
Stopwatch sw = new Stopwatch(); Type type = typeof(Program); sw.Start(); Action<int> action = type.CreateDelegate<Action<int>>("TestMethod"); for (int i = 0; i < 10000; i++) { action(i); } sw.Stop(); Console.WriteLine("DelegateBuilder:{0} ms", sw.ElapsedMilliseconds); sw.Start(); MethodInfo method = type.GetMethod("TestMethod"); for (int i = 0; i < 10000; i++) { method.Invoke(null, new object[] { i }); } sw.Stop(); Console.WriteLine("Reflection:{0} ms", sw.ElapsedMilliseconds);
这里将构造委托的过程和反射得到 MethodInfo 的过程都放在了循环的外面,是因为它们只需要获取一次,就可以一直使用的(也就是所谓的“预处理”)。至于时候将它们放在 StopWatch 的 Start 和 Stop 之间,就看是否想将预处理所需的时间也计算在内了。
目前我能想到的问题就这三个了,如果还有什么其它相关问题,可以联系我。
言归正传,下面就来分析如何为反射构造出相应的委托。为了简便起见,我将使用 Expression Tree 来构造委托,这样更加易读,而且效率也并不会比 Emit 低多少。对于 Expression 不熟悉的朋友可以参考 Expression 类。
一、从 MethodInfo 创建方法的委托
首先从创建方法的委托说开来,因为方法的委托显然是最常用、最基本的了。Delegate 类为我们提供了一个很好的参考,它的 CreateDelegate 方法有十个重载,这些重载之间的关系可以用下面的图表示出来,他们的详细解释可见 MSDN:
图1 Delegate.CreateDelegate
这些方法的确很给力,用起来也比较方便,尽管在我看来还不够强大:)。为了易于上手,自己的方法委托创建方法的行为也应该类似于 Delegate.CreateDelegate 方法,因此接下来会先分析 CreateDelegate 方法的用法,然后再解释如何自己创建委托。
1.1 创建开放的方法委托
CreateDelegate(Type, MethodInfo) 和 CreateDelegate(Type, MethodInfo, Boolean) 的功能是相同的,都是可以创建静态方法的委托,或者是显式提供实例方法的第一个隐藏参数(称开放的实例方法,从 .Net Framework 2.0 以后支持)的委托。以下面的类为例:
class TestClass { public static void TestStaticMethod(string value) {} public void TestMethod(string value) {} }
要创建 TestStaticMethod 方法的委托,需要使用 Action<string> 委托类型,代码为
Delegate.CreateDelegate(typeof(Action<string>), type.GetMethod("TestStaticMethod"))
得到的委托的效果与 TestStaticMethod(arg1) 相同。
要创建 TestMethod 方法的委托,则需要使用 Action<TestClass, string> 委托类型才可以,第一个参数表示要在其上调用方法的 TestClass 的实例:
Delegate.CreateDelegate(typeof(Action<TestClass, string>), type.GetMethod("TestMethod"))
得到的委托的效果与 arg1.TestMethod(arg2) 相同。
这个方法的用法很明确,自己实现起来也非常简单:
首先对开放的泛型方法构造相应的封闭的泛型方法,做法与上一篇《C# 使用 Binder 类自定义反射》中的 2.2.2 处理泛型方法 一节使用的算法相同,这里就不再赘述了。
接下就可以直接利用 Expression.Call 创建一个方法调用的委托,并对每个参数添加一个强制类型转换(Expression.Convert)即可。需要注意的是如果 MethodInfo 是实例方法,那么第一个参数要作为实例使用。最后用 Expression 构造出来的方法应该类似于:
// method 对应于静态方法。 returnType MethodDelegate(PT0 p0, PT1 p1, ... , PTn pn) { return method((T0)p0, (T1)p1, ... , (Tn)pn); } // method 对应于实例方法。 returnType MethodDelegate(PT0 p0, PT1 p1, ... , PTn pn) { return ((T0)p0).method((T1)p1, ... , (Tn)pn); }
构造开放的方法委托的核心方法如下所示:
private static Delegate CreateOpenDelegate(Type type, MethodInfo invoke, ParameterInfo[] invokeParams, MethodInfo method, ParameterInfo[] methodParams) { // 要求参数数量匹配,其中实例方法的第一个参数用作传递实例对象。 int skipIdx = method.IsStatic ? 0 : 1; if (invokeParams.Length == methodParams.Length + skipIdx) { if (method.IsGenericMethodDefinition) { // 构造泛型方法的封闭方法,对于实例方法要跳过第一个参数。 Type[] paramTypes = GetParameterTypes(invokeParams, skipIdx, 0, 0); method = method.MakeGenericMethodFromParams(methodParams, paramTypes); if (method == null) { return null; } methodParams = method.GetParameters(); } // 方法的参数列表。 ParameterExpression[] paramList = GetParameters(invokeParams); // 构造调用参数列表。 Expression[] paramExps = GetParameterExpressions(paramList, skipIdx, methodParams, 0); if (paramExps != null) { // 调用方法的实例对象。 Expression instance = null; if (skipIdx == 1) { instance = ConvertType(paramList[0], method.DeclaringType); if (instance == null) { return null; } } Expression methodCall = Expression.Call(instance, method, paramExps); methodCall = GetReturn(methodCall, invoke.ReturnType); if (methodCall != null) { return Expression.Lambda(type, methodCall, paramList).Compile(); } } } return null; }
1.2 创建第一个参数封闭的方法委托
CreateDelegate(Type, Object, MethodInfo) 和 CreateDelegate(Type, Object, MethodInfo, Boolean) 是最灵活的创建委托的方法,可以创建静态或实例方法的委托,可以提供或不提供第一个参数。先来给出所有用法的示例:
class TestClass { public static void TestStaticMethod(string value) {} public void TestMethod(string value) {} }
对于 TestStaticMethod (静态方法)来说:
- 若 firstArgument 不为 null,则在每次调用委托时将其传递给方法的第一个参数,此时称为通过第一个参数封闭,要求委托的签名包括方法除第一个参数之外的所有参数,使用方法为
Delegate.CreateDelegate(typeof(Action), "str", type.GetMethod("TestStaticMethod"))
- 若 firstArgument 为 null,且委托和方法的签名匹配(即所有参数类型都兼容),则此时称为开放的静态方法委托,使用方法为
Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestStaticMethod"))
- 若 firstArgument 为 null,且委托的签名以方法的第二个参数开头,其余参数类型都兼容,则此时称为通过空引用封闭的委托,使用方法为
Delegate.CreateDelegate(typeof(Action), null, type.GetMethod("TestStaticMethod"))
对于 TestMethod (实例方法)来说:
- 若 firstArgument 不为 null,则 firstArgument 被传递给隐藏的实例参数(就是 this),这时成为封闭的实例方法,要求委托的签名必须和方法的签名匹配,使用方法为
Delegate.CreateDelegate(typeof(Action<string>), new TestClass(), type.GetMethod("TestMethod"))
- 若 firstArgument 为 null,且委托显示包含方法的第一个隐藏参数(就是 this),则此时称为开放的实例方法委托,使用方法为
Delegate.CreateDelegate(typeof(Action<TestClass, string>), null, type.GetMethod("TestMethod"))
- 若 firstArgument 为 null,且委托的签名与方法的签名匹配,则此时称为通过空引用封闭的委托,使用方法为
Delegate.CreateDelegate(typeof(Action<string>), null, type.GetMethod("TestMethod"))
将以上六点总结来看,就是根据方法是静态方法还是实例方法,以及委托与方法签名的匹配方式就可以决定如何构造委托了。下面就是判断的流程图:
图2 方法委托的流程图
对于开放的静态或实例方法,可以使用上一节完成的方法;对于封闭的静态或实例方法,做法也比较类似,只要将 firstArgument 作为静态方法的第一个参数或者是实例使用即可;在流程图中特地将通过空引用封闭的实例方法拿出来,是因为 Expression 不能实现对 null 调用实例方法,只能够使用 Delegate.CreateDelegate 来生成委托,然后在外面再套一层自己的委托以实现强制类型转换。这么做效率肯定会更低,但毕竟这种用法基本不可能见到,这里仅仅是为了保证与 CreateDelegate 的统一。
1.3 创建通用的方法委托
这里我多加了一个方法,就是创建一个通用的方法委托,这个委托的声明如下:
public delegate object MethodInvoker(object instance, params object[] parameters);
通过这个委托,就可以调用任意的方法了。要实现这个方法也很简单,只要用 Expression 构造出类似于下面的方法即可。
object MethodDelegate(object instance, params object[] parameters) { // 检查 parameters 的长度。 if (parameters == null || parameters.Length != n + 1) { throw new TargetParameterCountException(); } // 调用方法。 return instance.method((T0)parameters[0], (T1)parameters[1], ... , (Tn)parameters[n]); }
对于泛型方法,显然无法进行泛型参数推断,直接报错就好;对于静态方法,直接无视 instance 参数就可以。
public static MethodInvoker CreateDelegate(this MethodInfo method) { ExceptionHelper.CheckArgumentNull(method, "method"); if (method.IsGenericMethodDefinition) { // 不对开放的泛型方法执行绑定。 throw ExceptionHelper.BindTargetMethod("method"); } // 要执行方法的实例。 ParameterExpression instanceParam = Expression.Parameter(typeof(object)); // 方法的参数。 ParameterExpression parametersParam = Expression.Parameter(typeof(object[])); // 构造参数列表。 ParameterInfo[] methodParams = method.GetParameters(); Expression[] paramExps = new Expression[methodParams.Length]; for (int i = 0; i < methodParams.Length; i++) { // (Ti)parameters[i] paramExps[i] = ConvertType( Expression.ArrayIndex(parametersParam, Expression.Constant(i)), methodParams[i].ParameterType); } // 静态方法不需要实例,实例方法需要 (TInstance)instance Expression instanceCast = method.IsStatic ? null : ConvertType(instanceParam, method.DeclaringType); // 调用方法。 Expression methodCall = Expression.Call(instanceCast, method, paramExps); // 添加参数数量检测。 methodCall = Expression.Block(GetCheckParameterExp(parametersParam, methodParams.Length), methodCall); return Expression.Lambda<MethodInvoker>(GetReturn(methodCall, typeof(object)), instanceParam, parametersParam).Compile(); }
二、从 ConstructorInfo 创建构造函数的委托
创建构造函数的委托的情况就很简单了,构造函数没有静态和实例的区分,不存在泛型方法,而且委托和构造函数的签名一定是匹配的,实现起来就如同 1.1 创建开放的方法委托,不过这是用到的实 Expression.New 方法而不是 Expression.Call 了。
public static Delegate CreateDelegate(Type type, ConstructorInfo ctor, bool throwOnBindFailure) { ExceptionHelper.CheckArgumentNull(ctor, "ctor"); CheckDelegateType(type, "type"); MethodInfo invoke = type.GetMethod("Invoke"); ParameterInfo[] invokeParams = invoke.GetParameters(); ParameterInfo[] methodParams = ctor.GetParameters(); // 要求参数数量匹配。 if (invokeParams.Length == methodParams.Length) { // 构造函数的参数列表。 ParameterExpression[] paramList = GetParameters(invokeParams); // 构造调用参数列表。 Expression[] paramExps = GetParameterExpressions(paramList, 0, methodParams, 0); if (paramExps != null) { Expression methodCall = Expression.New(ctor, paramExps); methodCall = GetReturn(methodCall, invoke.ReturnType); if (methodCall != null) { return Expression.Lambda(type, methodCall, paramList).Compile(); } } } if (throwOnBindFailure) { throw ExceptionHelper.BindTargetMethod("ctor"); } return null; }
与通用的方法委托类似的,我也使用下面的委托
public delegate object InstanceCreator(params object[] parameters);
来创建通用的构造函数的委托,与通用的方法委托的实现也很类似。
public static Delegate CreateDelegate(Type type, ConstructorInfo ctor, bool throwOnBindFailure) { ExceptionHelper.CheckArgumentNull(ctor, "ctor"); CheckDelegateType(type, "type"); MethodInfo invoke = type.GetMethod("Invoke"); ParameterInfo[] invokeParams = invoke.GetParameters(); ParameterInfo[] methodParams = ctor.GetParameters(); // 要求参数数量匹配。 if (invokeParams.Length == methodParams.Length) { // 构造函数的参数列表。 ParameterExpression[] paramList = GetParameters(invokeParams); // 构造调用参数列表。 Expression[] paramExps = GetParameterExpressions(paramList, 0, methodParams, 0); if (paramExps != null) { Expression methodCall = Expression.New(ctor, paramExps); methodCall = GetReturn(methodCall, invoke.ReturnType); if (methodCall != null) { return Expression.Lambda(type, methodCall, paramList).Compile(); } } } if (throwOnBindFailure) { throw ExceptionHelper.BindTargetMethod("ctor"); } return null; }
三、从 PropertyInfo 创建属性的委托
有了创建方法的委托作为基础,创建属性的委托就非常容易了。如果委托具有返回值那么意味着是获取属性,不具有返回值(返回值为 typeof(void))意味着是设置属性。然后利用 PropertyInfo.GetGetMethod 或 PropertyInfo.GetSetMethod 来获取相应的 get 访问器或 set 访问器,最后直接调用创建方法的委托就可以了。
封闭的属性委托也同样很有用,这样可以将属性的实例与委托绑定。
对于属性并没有创建通用的委托,是因为属性的访问分为获取和设置两部分的,这两部分难以有效的结合到一块。
四、从 FieldInfo 创建字段的委托
在创建字段的委托时,就不能使用现有的方法了,而必须用 Expression.Assign 自己完成字段的赋值。字段的委托同样可以分为开放的字段委托和使用第一个参数封闭的字段委托,其判断过程如下:
图3 字段委托流程图
字段的处理很简单,就是通过 Expression.Field 访问字段,然后通过 Expression.Assign 对字段进行赋值,或者直接返回字段的值。图中单独列出来的“通过空引用封闭的实例字段”,同样是因为不能用代码访问空对象的实例字段,这显然是个毫无意义的操作,不过为了与通过空引用封闭的属性得到的结果相同,这里总是抛出 System.NullReferenceException。
五、从 Type 创建成员委托
这个方法提供了创建成员委托的最灵活的方式,它可以根据给出的成员名称、BindingFlags 和委托的签名决定是创建方法、构造函数、属性还是字段的委托。
它的做法就是,依次利用 PowerBinder.Cast 在 type 中查找与给定委托签名匹配的方法、属性和字段,并尝试为每个匹配的成员构造委托(使用前面四个部分中给出的方法)。当某个成员成功构造出委托,那么它就是最后需要的那个。
由于 PowerBinder 可以支持查找泛型方法和显式类型转换,因此构造委托的时候也自然就能够支持泛型方法和显式类型转换了。
DelegateBuilder 构造委托的方法算是到此结束了,完整的源代码可见 DelegateBuilder.cs,总共大约 2500 行,不过其中大部分都是注释和各种方法重载(目前有 54 个重载),VS 代码度量的结果只有 509 行。