.Net 的反射是个很好很强大的东西,不过它的效率却实在是不给力。已经有很多人针对这个问题讨论过了,包括各种各样的 DynamicMethod 和各种各样的效率测试,不过总的来说解决方案就是利用 Expression Tree、Delegate.CreateDelegate 或者 Emit 构造出反射操作对应的委托,从而实现加速反射的目的。
虽然本篇文章同样是讨论利用委托来加速反射调用函数,不过重点并不在于如何提升调用速度,而是如何更加智能的构造出反射的委托,并最终完成一个方便易用的委托创建器 DelegateBuilder。
它的设计目标是:
能够对方法调用、构造函数调用,获取或设置属性和获取或设置字段提供支持。
能够构造出特定的委托类型,而不仅限于 Func 或者其它的 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(params T[] arg) { }
static void Main(string[] args) {
Type type = typeof(Program);
Action m1 = type.CreateDelegate>("TestMethod");
m1(10);
Program p = new Program();
Action m2 = type.CreateDelegate>("TestMethod");
m2(p, 10);
Action m3 = type.CreateDelegate>("TestMethod");
m3(p, 10);
Action m4 = type.CreateDelegate>("TestMethod", p);
m4(10);
MyDelegate m5 = type.CreateDelegate("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 action = type.CreateDelegate>("TestMethod");
for (int i = 0; i < 10000; i++)
{
action(i);
}
sw.Stop();
Console.WriteLine("DelegateBuilder:{0} ms", sw.ElapsedM