在对Closure的再思考里面我提到了说网上有观点认为用lambda表达式声明的“递归”实际上并不是真正的递归。本文针对这个观点做专门的研究。
传统的递归
所谓传统的递归,是指一直来我们所经常使用的经典结构的递归。以n的阶乘来作为例子说吧,“传统”的递归结构可以用如下的代码表示:
public static int FacRecursive(int n) { if (n <= 1) return 1; return FacRecursive(n - 1) * n; }
对于这个结构,是没有任何问题的。FacRecursive(5)就表示为计算5的阶乘,函数自身的调用顺序和参数传递可以用下图表示:
变相的递归
理解了传统的递归后,再来看看如下函数所表示的意义:
public static int Fac(int n) { if (n <= 1) return 1; return FacShadow(n - 1) * n; } public static int FacShadow(int n) { return Fac(n); }
应该不难理解,假如从Fac函数入口,程序执行便产生了Fac和FacShadow函数之间依次互相调用的情况,其最终计算结果也是n的阶乘。对于表达式Fac(5)的函数执行调用序列可用下图来表示:
那么对于这种结构,它算不算递归呢?事实上,函数FacShadow在这个例子中起到了一个“路由”的作用,它帮助函数Fac成功实现了调用自身的效果。从这种意义上来说,它应该是一个递归。但从另一个方面来讲,Fac和FacShadow是两个平等的函数,二者共同协作(通过特定的顺序互相调用)完成了阶乘的计算工作,这样理解来,递归的概念就有所牵强。无论如何,是不是递归的概念已经意义不大,真正的理解了代码的机制就可以了。
代理形成的递归
代理的出现使得对函数的操作如同对普通变量的操作一样方便。因此,“递归”又有了第三种实现形式:
public static Func<int, int> FacFunc = null; public static int FacMethod(int n) { if (n <= 1) return 1; return FacFunc(n - 1) * n; } FacFunc = FacMethod;
这与第二种“代理”相比有何不同呢?可以看出,在这种情况下,充当“路由”功能的是一个代理,而不是一个函数。又由于代理是函数的一个“签名”,其本身并没有函数的本体(而第二种中,FacShadow却是一个完整的函数),其“路由”的方式更直接,或者说原函数实现调用自身的方式比起第二种方式来说更直接一些。下图是FacFunc的执行过程图示:
其中表示由代理解析成具体函数的一个过程。那么这种情况算不算递归呢?个人之见:这完全称的上递归。
执行效率的比较
了解了三种形式的“递归”之后,来比较一下它们的执行效率情况。根据它们的特点预先估计一下:第一种最直接,所以执行最快;第二种执行路径最多,执行最慢。所以它们执行效率的关系为:
“传统递归” > “代理路由型递归” > “函数路由型递归”。
实际的运行情况还需代码来检测,使用如下代码:
using System; using System.Diagnostics; namespace ConsoleApplication2 { class Program { static void Main(string[] args) { // initialize delegate routed recursion FacFunc = FacMethod; // create a lambda expression generated delegate routed recursion Func<int, int> funcLambda = null; funcLambda = n => n <= 1 ? 1 : n * funcLambda(n - 1); const int facIterations = 99999; const int v = 10; double typicalRecursion = 0; double methodRecursion = 0; double delegateRecursion = 0; double lambdaRecursion = 0; const int iterations = 500; for (int iteration = 0; iteration < iterations; ++iteration) { // measure typical recursion System.Diagnostics.Stopwatch watch = Stopwatch.StartNew(); for (int i = 0; i < facIterations; i++) { FacRecursive(v); } watch.Stop(); typicalRecursion += watch.ElapsedMilliseconds; // measure delegate routed recursion watch = Stopwatch.StartNew(); for (int i = 0; i < facIterations; i++) { FacFunc(v); } watch.Stop(); delegateRecursion += watch.ElapsedMilliseconds; // measure lambda expression generated delegate routed recursion watch = Stopwatch.StartNew(); for (int i = 0; i < facIterations; i++) { funcLambda(v); } watch.Stop(); lambdaRecursion += watch.ElapsedMilliseconds; // measure method routed recursion watch = Stopwatch.StartNew(); for (int i = 0; i < facIterations; i++) { Fac(v); } watch.Stop(); methodRecursion += watch.ElapsedMilliseconds; } Console.WriteLine("typical recursion: " + typicalRecursion / iterations); Console.WriteLine("Delegate routed recursion: " + delegateRecursion / iterations); Console.WriteLine("Lambda delegate routed recursion: " + lambdaRecursion / iterations); Console.WriteLine("Method routed recursion: " + methodRecursion / iterations); Console.ReadKey(); } // typical recursion public static int FacRecursive(int n) { if (n <= 1) return 1; return FacRecursive(n - 1) * n; } // method routed recursion public static int Fac(int n) { if (n <= 1) return 1; return FacShadow(n - 1) * n; } public static int FacShadow(int n) { return Fac(n); } // delegate routed recursion public static Func<int, int> FacFunc = null; public static int FacMethod(int n) { if (n <= 1) return 1; return FacFunc(n - 1) * n; } } }代码比较长,做一下解释。测量一张纸的厚度很难,且不准确,可以通过测量一累纸的厚度然后除以张数来计算单张纸的厚度。同理,单个函数的执行时间很难准确测量,所以可以循环累加测量,然后取平均值,这就是代码中iterations = 500这个变量的用途。另一个要点,当时间很短时(比如小于1ms),其测量值不具代表意义,这就是代码中facIterations = 99999这个变量的用途,用来多次执行函数,我们把这个多次执行的时间作为参考来对比,当然不是直接对比,而是做iterations次再循环后取平均。看看结果如何:
在Debug模式下输出:
其结果跟前面分析的一致。接下来看看Release下的输出如何:
这跟Debug模式下有很大的区别,可以看出在Release下编译器对程序做了优化,其中Method reouted recursion优化的最佳,其达到的效果几乎跟传统递归一样。其次是Lambda delegate routed recursion,优化之后其效率居然比手动写的Delegate routed recursion还高,而两者的结构却是一样的。