8.2.3 在 F# 使用闭包捕获状态
在这一节,我们要讨论闭包(closures),在函数编程中,这是一个非常重要的概念。闭包很常见,大部分时间它们都不使用可变状态;不过,出于实用的考虑,使用可变状态有时也是需要的,闭包能够提供了一种极好的方式,限制可变状态的作用域。
首先,我们看一段简单的 F# 代码片段,在第五章已经看过:
> let createAdder num =
(fun m -> num +m);;
val createAdder : int -> int –> int
在我们前面的讨论中,我们没有看出,像这样写的函数,与调用加法,取两个参数并返回和的函数之间有什么区别。这是因为,我们能够只用一个参数值调用加法函数:因为有了散应用,其结果是一个函数,将特定数加到任何给定的参数值上。
如果仔细分析一下前面示例的返回结果,它不仅是函数的代码!代码是一组指令,加两个数字,但是,如果我们用两个不同的参数,调用 createAdder 两次,返回的函数显然不同,因为,它们加的数不同。关键的思想是,函数不仅是代码,也是闭包,包含了由函数使用、但不在函数体内声明的值。由闭包控制的值称为捕获(captured)。在前面的例子中,唯一的捕获是 num 参数。
闭包听起来复杂,实际上简单。F# 编译器使用抽象类 FastFunc<int, int> 表示参数为整数,返回结果为整数的函数值。清单 8.7 显示了createAdder函数码转换为 C# 生成的代码。
清单 8.7 F# 编译器生成的闭包类 (C#)
class createAdder : FastFunc<int,int> {
public int num; <-- 保存捕获的值
internal createAdder(int num){ [1]
this.num = num;
}
public override int Invoke(int m) { <-- 运行构造的函数
return this.num +m;
}
}
static FastFunc<int, int>createAdder(int num) { [2]
return new createAdder(num);
}
编译器生成一个静态方法createAdder[2],相当于 F# 的函数。方法构造了一个函数值,由函数代码组成,保存了闭包捕获的值。生成的闭包类的参数取为捕获的值,在我们的示例中,只有一个参数 num[1]。在用虚拟方法 Invoke 运行这个函数值时,代码可以访问保存在闭包中的值。
当然,自从我们开始讨论lambda 函数,就一直在使用闭包创建函数,只是没有明确地讲出来,因为,通常不需要考虑这些,它们确实就在运行。然而,如果闭包捕获到可变值,又会怎样呢?