在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
闭包的概念出现于60年代,最早实现闭包的程序语言是Scheme。之后,闭包被广泛使用于函数式编程语言如ML语言和LISP。很多命令式程序语言也开始支持闭包。
在一些语言中,在函数中定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。
词源
Peter J. Landin 在1964年将术语 闭包 定义为一种包含 环境成分 和 控制成分的实体,用于在他的SECD 机器上对表达式求值。[1] Joel Moses 认为是 Landin 发明了 闭包 这一术语,用来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的 lambda 表达式,从而形成了 闭合的表达式,或称闭包。[2][3] 这一用法后来于 1975 年被 Sussman 和 Steele 在定义 Scheme 语言的时候予以采纳。[4] 并广为流传。
闭包和状态表达
闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
不过,用这种方式来使用闭包时,闭包不再具有引用透明性,因此也不再是纯函数。即便如此,在某些“近似于函数式编程语言”的语言,例如Scheme中,闭包还是得到了广泛的使用。
闭包和第一级函数
闭包通常出现在将函数当作第一级对象的语言中——在这些语言中,函数可以被当作参数传递,也可以作为返回值返回,就像字符串、整数等简单类型。例如以下Scheme代码:
; Return a list of all books with at least THRESHOLD copies sold. (define (best-selling-books threshold) (filter (lambda (book) (>= (book-sales book) threshold)) book-list))
在这个例子中,lambda表达式(lambda (book) (>= (book-sales book) threshold))
出现在函数best-selling-books
中。当这个lambda表达式被执行时,Scheme创造了一个包含此表达式以及对threshold
变量的引用的闭包,其中threshold
变量在lambda表达式中是自由变量。
这个闭包接着被传递到filter
函数。这个函数的功能是重复调用这个闭包以判断哪些书需要增加到列表那些需要丢弃。因为闭包中引用了变量threshold
,所以它在每次被filter
调用时都可以使用这个变量,虽然filter
可能定义在另一个文件中。
下面是用ECMAScript (JavaScript)写的同一个例子:
// Return a list of all books with at least 'threshold' copies sold. function bestSellingBooks(threshold) { return bookList.filter( function (book) { return book.sales >= threshold; } ); }
这里,关键字function
取代了lambda
,Array.filter
方法[5]取代了filter
函数,但两段代码的功能是一样的。
一个函数可以创建一个闭包并返回它,就像这样:
// Return a function that approximates the derivative of f // using an interval of dx, which should be appropriately small. function derivative(f, dx) { return function (x) { return (f(x + dx) - f(x)) / dx; }; }
因为在这个例子中闭包已经超出了创建它的函数的范围,所以变量f
和dx
将在函数derivative
返回后继续存在。在没有闭包的语言中,变量的生命周期只限于创建它的环境。但在有闭包的语言中,只要有一个闭包引用了这个变量,它就会一直存在。清理不被任何函数引用的变量的工作通常由垃圾回收完成。
闭包的用途
- 因为闭包只有在被调用时才执行操作,所以它可以被用来定义控制结构。例如:在Smalltalk语言中,所有的控制结构,包括分歧条件(if/then/else)和循环(while和for),都是通过闭包实现的。用户也可以使用闭包定义自己的控制结构。
- 多个函数可以使用一个相同的环境,这使得它们可以通过改变那个环境相互交流。比如在Scheme中:
(define foo #f) (define bar #f) (let ((secret-message "none")) (set! foo (lambda (msg) (set! secret-message msg))) (set! bar (lambda () secret-message))) (display (bar)) ; prints "none" (newline) (foo "meet me by the docks at midnight") (display (bar)) ; prints "meet me by the docks at midnight"
其它语言中类似闭包的结构
在C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的void*
指针用以保存用户数据。这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。
一些面向对象的语言包含了一些闭包的特性,例如:
C
C语言 (使用LLVM编译器或苹果修改版的GCC)支持块。闭包变量用__block标记。同时,这个扩展也可以应用到Objective-C与C++中。
typedef int (^IntBlock)(); IntBlock downCounter(int start) { __block int i = start; return Block_copy( ^int() { return i--; }); } IntBlock f = downCounter(5); printf("%d", f()); printf("%d", f()); printf("%d", f()); Block_release(f);
C++
C++允许通过重载operator()
来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。它们可以在运行时创建,保存状态,但是不能如闭包一般隐式获取局部变量。C++标准委员会正在考虑两种在C++中引入闭包的建议(它们都称为lambda函数)[1], [2]。这些建议间主要的区别在于一种默认在闭包中储存全部局部变量的拷贝,而另一种只存储这些变量的引用。这两种建议都提供了可以覆盖默认行为的选项。若这两种建议之一被接受,则可以写如下代码
void foo(string myname) { typedef vector<string> names; int y; names n; // ... names::iterator i = find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;}); // 'i' is now either 'n.end()' or points to the first string in 'n' // 'i' 现在是'n.end()'或指向'n'中第一个 // 不等于'myname'且长度大于'y'的字符串 }
至少两种C++编译器,Visual C++ 2010(或Visual C++ 10.0)与gcc-4.5已经支持了这种特性。
外部链接