如果一个程序设计语言能够用高阶函数解决问题,则意味着数据作用域问题已十分突出。当函数可以当成参数和返回值在函数之间进行传递时,编译器利用闭包扩展变量的作用域,以保证随时能得到所需要的数据。
C#函数式程序设计之作用域
在C#中,变量的作用域是严格确定的。其本质是所有代码生存在类的方法中、所有变量只生存于声明它们的模块中或者之后的代码中。变量的值是可变的,一个变量越是公开,带来的问题就越严重。一般的原则是,变量的值最好保持不变,或者在最小的作用域内保存其值。一个纯函数最好只使用在自己的模块中定义的变量值,不访问其作用域之外的任何变量。
遗憾的是,有时我们无法把变量的值限制于函数的范围内。如果在程序的初始化时定义了几个变量,在后面需要反复用到它们,怎么办?一个可能的办法是使用闭包。
C#函数式程序设计之闭包机制
为了理解闭包的本质,我们分析几个使用闭包的例子:
namespace Closures { class ClosuresClass { static void ClosuresTest() { Console.WriteLine(GetClosureFunc()(30)); } static Func<int,int> GetClosureFunc() { int val = 10; Func<int, int> internalAdd = x => x + val; Console.WriteLine(internalAdd(10)); val = 30; Console.WriteLine(internalAdd(10)); return internalAdd; } } }
此代码的结果输出是多少?答案是20 40 60,前面两个值,大家应该很容易就能看出来,但第三个值为什么是60呢?先来看看程序的执行流程:Closures函数调用GetClosureFunc函数并进入其中。函数调用语句中带了一个参数30。这是由于GetClosureFunc返回的是一个函数,即执行时再次调用了这个函数,进入GetClosureFunc函数中,首先val的值为10,通过internalAdd方法传入一个值10,因此第一个输出值为20,往下走,val的值变成30,通过internalAdd方法传入值10,于是第二个输出值为40。从这里我们大致可以看出,局部函数和局部变量如何在同一个作用域中起作用,显然,对局部变量的改变会影响internalAdd的值,尽管变量的改变发生在internalAdd最初的创建之后。最后,GetClosureFunc返回了internalAdd方法,以参数30再次调用这个函数,于是,结果成为60。
初看起来,这并不真正符合逻辑。val应该是一个局部变量,它生存在栈中,当GetClosureFunc函数返回时,它就不在了,不是么?确实如此,这正是闭包的目的,当编译器会明白无误地警告这种情况会引起程序的崩溃时阻止变量值超出其作用域之外。
从技术角度来看,数据保存的位置很重要,编译器创建一个匿名类,并在GetClosureFunc中创建这个类的实例——如果不需要闭包起作用,则那个匿名函数只会与GetClosureFunc生存在同一个类中,最后,局部变量val实际上不再是一个局部变量,而是匿名类中的一个字段。其结果是,internalAdd现在可以引用保存在匿名类实例中的函数。这个实例中也包含变量val的数据。只要保持internalAdd的引用,变量val的值就一直保存着。
下面这段代码说明编译器在这种情形下采用的模式:
private sealed class DisplayClass { public int val; public int AnonymousFunc(int x) { return x + this.val; } private static Func<int, int> GetClosureFunc() { DisplayClass displayClass = new DisplayClass(); displayClass.val = 10; Func<int, int> internalAdd = displayClass.AnonymousFunc; Console.WriteLine(internalAdd(10)); displayClass.val = 30; Console.WriteLine(internalAdd(10)); return internalAdd; } }
回到动态创建函数思想:现在可以凭空创建新的函数,而且它的功能因参数而异。例如,下面这个函数把一个静态值加到一个参数上:
private static void DynamicAdd() { var add5 = GetAddX(5); var add10 = GetAddX(10); Console.WriteLine(add5(10)); Console.WriteLine(add10(10)); } private static Func<int,int> GetAddX(int staticVal) { return x => staticVal + x; }
这个原理正是许多函数构建技术的基础,这种方法显然与方法重载等面向对象方法相对应。但是与方法重载不同,匿名函数的创建可以在运行时动态发生,只需受另一个函数中的一行代码触发。为使某个算法更加容易读和写而使用的特殊函数可以在调用它的方法中创建,而不是再类级别上胡乱添加函数或方法——这正是函数模块化的核心思想。
总结
闭包是程序设计语言支持函数式设计方法的一个重要工具。