前言
在一些时候,我们需要动态生成一个函数,例如最近银河发表的一篇随笔当中提及到的《画函 数图形的C#程序(改进版)》。不久之前,我们伟大的老赵也发过一篇《方 法的直接调用,反射调用与……Lambda表达式调用》,他也推荐了《Dynamic Reflection Library》,但 我不懂怎么应用在这里,因为这里的输入是字符串。自己用 CodeDom 实现了一下(其实这个我在没有来博客园之前就想实现了,当时刚学 .Net 不久,走了很多弯路才勉强可行,博客园真是个学习和提高的好地方^_^),发现性能提高了非常多,故在这里跟大家分享一下。
思想和目标
首先,普遍知识,MethodInfo.Invoke 需要查找元数据,很慢,然而通过一个委托调用某方法跟调用这个方法的速度差不多。因此,在需要经常重复调用一个方法的时候,不应该使用 MethodInfo.Invoke 这个方法。
然而,偏偏这个方法是由用户动态输入的,那么,解决的思路是把用户输入编译成动态程序集中的一个方法,却不直接 MethodInfo.Invoke 它,而是在这个动态程序集中另外编写一个方法,返回一个委托,指向目标方法,然后 MethodInfo.Invoke 这个另外编写的方法,获得一个委托,之后就可以重复调用我们动态生成的目标方法却不用 MethodInfo.Invoke 了,而且是 TypeSafe 的,还有 IDE 的智能提示支持,多好!
在没有 Lambda 表达式甚至没有匿名方法的时代(就是我刚有这个想法的时代),要实现这个很麻烦,这是因为委托的类型问题。(见过“XYZ 类型不能强制转换为 XYZ 类型”这种错误吗? XYZ = XYZ)
自从有了 Linq 以后,BCL 中多了一系列形如 Func<T, TResult> 这样的委托类型,这种思想的实现就更容易了,Lambda 表达式还能免去我们多写一个方法的痛苦,实在太好了。
那还等什么?编程实现!
首先,编写核心的动态程序集代码以及编译。
private static Assembly getDynamicAssembly()
{
string source = @"
public class DynamicClass
{
public static System.Func<double, double> GetFunc()
{
return ( double x ) =>
{
return x + x;
};
}
public static double Add( double x )
{
return x + x;
}
}";
var providerOptions = new Dictionary<string, string> { { "CompilerVersion", "v3.5" } };
var cp = new CSharpCodeProvider(providerOptions);
CompilerParameters cps = new CompilerParameters();
cps.GenerateExecutable = false;
cps.GenerateInMemory = true;
cps.IncludeDebugInformation = false;
cps.CompilerOptions += "/optimize /reference:System.Core.dll";
return cp.CompileAssemblyFromSource(cps, source).CompiledAssembly;
}
说明一下,private static 是因为我还没有封装,只是写了一个 ConsoleApp 来证明想法。OK,source 里的明星方法是 GetFunc(),它返回一个 Func<double, double> 委托,委托指向的方法由 Lambda 表达式构造,这个 Lambda 表达式的内容就是我们要动态生成的内容。另外,在使用 CSharpCodeProvider 的时候默认是用 2.0 的,所以需要配置一下,具体可以查阅 MSDN Library,这里就直接 hard code 进去了。实际应用的时候,动态编译可能会出错,这里也忽略了。Add 方法是等一下要 MethodInfo.Invoke 的对比方法。
接着,就是编写个方法测试一下效果,很简单:
private static Stopwatch measureDynamicDelegate( Assembly asm, int length )
{
var func = asm.GetType("DynamicClass").GetMethod("GetFunc").Invoke(null, null) as Func<double, double>;
var sw = Stopwatch.StartNew();
for ( int i = 0; i < length; i++ )
{
func(1d);
}
sw.Stop();
return sw;
}
可以看到,要使用很简单,只需写 func(1d),而且还有 IDE 的 Intellisense 支持:
然后就是其他测试了。这里就不贴出来了,代码等下提供下载。
测试结果
我分别在 Debug 和 Release 模式中测试了一下,发现相对差别不大,都是首次调用慢一点,以后的调用就快一点。左图为 Debug 模式,右图为 Release 模式,循环 65536 次。(点击放大)