这是十年前学习 Scala 时写下的一篇文章,现在读来依然正确且深刻,本文是为接下来介绍 Rust 闭包而准备的一篇预热文章,也是直击“闭包”本质的一篇文章。
1. 定义
关于闭包有太多种解释,但基本上都很难用一两句解释清楚,下面这句简短的定义是我见过的最精炼且准确的解释了:
A closure is a function that carries an implicit binding to all the variables referenced within it. In other words, the function (or method) encloses a context around the things it references.
翻译过来就是:
一个“闭包”就是一个函数,但同时它还会以一种“隐式绑定”的形式一起“携带”上这个函数内部引用的所有变量,也就是说:闭包会围绕该函数(或方法)及其所引用的变量封装一个上下文环境。
首先,闭包是一个函数,然后,也是最本质的地方:这个函数内部会引用(依赖)到一些变量,这些变量既不是全局的也不是局部的,而是在定义在上下文中的(这种变量被称为“自由变量”,我们会在稍后的例子中看到这种变量),闭包的“神奇”之处是它可以cache或者说是持续的trace它所引用的这些变量。这些变量以及它们引用的对象不会被 GC 释放。同样是这件事情,换另一种说法就是:闭包是一个函数,但同时这个函数“背后”还自带了一个“隐式”的上下文保存了函数内部引用到的一些(自由)变量。
2. 第一个例子
3. 第二个例子
4. 对两个例子的补充
上述两个例子的代码都在解释闭包的概念,但是解释的角度不太一样,相对而言第二个例子揭示地更为深刻一些,它揭示了:闭包会持续地隐式追踪它所使用的那些自由变量。每当我们去调用一个闭包时,脑子里一定要意识到:闭包不单单是定义它的那段代码,同时还有一个绑定在它身上的“上下文”(环境),在那个上下文里有各种它所依赖的变量!
5. 更透彻地理解:闭包产生的根源
从某种角度上说,闭包是函数字面量的一个“衍生品”。函数字面量的存在使得函数的定义与普通变量无异,也就是val 变量名 = 函数字面量,既然普通变量在赋值时可以引用另一个变量的值,那么定义函数时,在函数字面量里引用其他变量也变成非常自然的事情(而在传统的函数体内是没有办法直接引用函数体外部的变量的),比如,像下面这样定义普通变量是非常常见的:
vara=1;varb=a+1;显然变量b的赋值过程中引用了变量a. 同样的,在函数编程语言里,像下面这样定义函数:
vara=1;valb=()=>a+1又有何不可呢?这时b就成了典型的闭包,它所引用的变量的a是定义在上下文的。
6. Spark 为什么需要闭包?
闭包被创造出来显然是因为有场景需要的。一个最为普遍和典型的使用场合是:推迟执行。我们可以把一段代码封装到闭包里,你可以等到“时机”成熟时去执行它。比如:在 Spark 里,针对 RDD 的计算任务都要分布到每个节点(准确的说是 executor)上并行处理,Spark就需要封装一个闭包,把相关的操作(方法)和需要的变量引入到闭包中分发给节点执行。