闭包 (计算机科学)

在计算机科学中，闭包（Closure），又称词法闭包（Lexical Closure）或函数闭包（function closures），是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体，它存储了一个函数（通常是其入口地址）和一个关联的环境（相当于一个符号查找表）。环境里是若干对符号和值的对应关系，它既要包括约束变量（该函数内部绑定的符号），也要包括自由变量（在函数外部定义但在函数内被引用），有些函数也可能没有自由变量。闭包跟函数最大的不同在于，当捕捉闭包的时候，它的自由变量会在捕捉时被确定，这样即便脱离了捕捉时的上下文，它也能照常运行。捕捉时对于值的处理可以是值拷贝，也可以是名称引用，这通常由语言设计者决定，也可能由用户自行指定（如C++）。
概述.
闭包的概念出现于60年代，最早实现闭包的程序语言是Scheme。之后，闭包被广泛使用于函数式编程语言如ML语言和LISP。很多命令式程序语言也开始支持闭包。
在支持头等函数的语言中，如果函数f内定义了函数g，那么如果g存在自由变量，且这些自由变量没有在编译过程中被优化掉，那么将产生闭包。
闭包和匿名函数经常被用作同义词。但严格来说，匿名函数就是字面意义上没有被赋予名称的函数，而闭包则实际上是一个函数的实例，也就是说它是存在于内存里的某个结构体。如果从实现上来看的话，匿名函数如果没有捕捉自由变量，那么它其实可以被实现为一个函数指针，或者直接内联到调用点，如果它捕捉了自由变量那么它将是一个闭包；而闭包则意味着同时包括函数指针和环境两个关键元素。在编译优化当中，没有捕捉自由变量的闭包可以被优化成普通函数，这样就无需分配闭包结构体，这种编译技巧被称为。
词源.
闭包的概念是在1960年代为采用lambda演算的表达式的机器求值而开发的，它首次在1970年于PAL编程语言中完全实现，用来支持词法作用域的头等函数。
彼得·兰丁（Peter Landin）在1964年将术语“闭包”定义为一种包含环境成分和控制成分的实体，用于在他的SECD机器上对表达式求值。
认为是Landin发明了“闭包”这一术语，用来指代某些其开放绑定（自由变量）已经由其语法环境完成闭合（或者绑定）的lambda表达式，从而形成了闭合的表达式，或称闭包。。这一用法后来于1975年被Sussman和Steele在定义 Scheme语言的时候予以采纳，并广为流传。
语义.
闭包和状态表达.
闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中，这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数，因此无法从其它程序代码部分进行访问。不过，变量的生存期是可以很长，在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点，闭包可以用来完成信息隐藏，并进而应用于需要状态表达的某些编程范型中。
不过，用这种方式来使用闭包时，闭包不再具有，因此也不再是纯函数。即便如此，在某些非纯函数式编程语言，例如Scheme中，闭包还是得到了广泛的使用。
闭包和头类函数.
典型的支持闭包的语言中，通常将函数当作头等函数——在这些语言中，函数可以被当作参数传递、也可以作为函数返回值、绑定到变量名、就像字符串、整数等简单类型。例如以下Scheme代码：
<syntaxhighlight lang="scheme">
</syntaxhighlight>
在这个例子中，lambda表达式codice_1出现在函数codice_2中。当这个lambda表达式被执行时，Scheme创造了一个包含此表达式以及对codice_3变量的引用的闭包，其中codice_3变量在lambda表达式中是自由变量。
这个闭包接着被传递到codice_5函数。这个函数的功能是重复调用这个闭包以判断哪些书需要增加到列表哪些书需要丢弃。因为闭包中引用了变量codice_3，所以它在每次被codice_5调用时都可以使用这个变量，虽然codice_5可能定义在另一个文件中。
下面是用ECMAScript （JavaScript）写的同一个例子：
<syntaxhighlight lang="javascript">
// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
</syntaxhighlight>
这里，关键字codice_9取代了codice_10，codice_11方法取代了codice_5函数，但两段代码的功能是一样的。
一个函数可以创建一个闭包并返回它，如下述JavaScript例子：
<syntaxhighlight lang="javascript">
// Return a function that approximates the derivative of f
// using an interval of dx, which should be appropriately small.
function derivative(f, dx) {
</syntaxhighlight>
因为在这个例子中闭包已经超出了创建它的函数的范围，所以变量codice_13和codice_14将在函数codice_15返回后继续存在。在没有闭包的语言中，变量的生命周期只限于创建它的环境。但在有闭包的语言中，只要有一个闭包引用了这个变量，它就会一直存在。清理不被任何函数引用的变量的工作通常由垃圾回收完成，但对于 C++ 这种没有垃圾收集（起码目前仍没有一个为语言本身所认可的）的语言而言也不是难事——通过一些细致而琐碎的步骤。
闭包的用途.
<syntaxhighlight lang="scheme">
(display (bar)) ; prints "none"
(display (bar)) ; prints "meet me by the docks at midnight"
</syntaxhighlight>
闭包的实现.
典型实现方式是定义一个特殊的数据结构，保存了函数地址指针与闭包创建时的函数的词法环境表示（那些非局部变量的绑定）。使用函数调用栈的语言实现闭包比较困难，因而这也说明了为什么大多数实现闭包的语言是基于垃圾收集机制——当然，不使用垃圾收集也可以做到。
闭包的实现与函数对象很相似。
通过将自由变量放进参数表、并扩大函数名字的作用域，可以把一个闭包 / 匿名 / 内部函数变成一个普通的函数，这叫做“”。例：
<syntaxhighlight lang="cpp">
void G(void){
//那么 fn 是一个闭包，指向那个匿名函数。
//这里 wstr 是自由变量，首先将其放入参数表：
void G(void){
//现在 fn 中没有自由变量了。把这个匿名函数取个名之后放到全局命名空间里：
wchar_t fn(size_t ui, const std::wstring &wstr){
void G(void){
//这就把 fn“提升”成了一个普通的函数。
</syntaxhighlight>
各种语言中（类似）闭包的结构.
C语言的回调函数.
在C语言中，支持回调函数的库有时在注册时需要两个参数：一个函数指针，一个独立的codice_16指针用以保存用户数据。这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包，但语法上有所不同。
gcc对C语言的扩展.
gcc编译器对C语言实现了一种闭包的程序特性。
C语言扩展：Blocks.
C语言 (使用LLVM编译器或苹果修改版的GCC)支持块。闭包变量用__block标记。同时，这个扩展也可以应用到Objective-C与C++中。
<syntaxhighlight lang="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);
</syntaxhighlight>
C++函数对象.
C++早期标准允许通过重载codice_17来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。它们可以在运行时动态创建、保存状态，但是不能如闭包一般方便地隐式获取局部变量，并且有“专物专用”的繁琐问题——对于每一段闭包代码都要单独写一个函数对象类。
C++11标准已经支持了闭包，这是一种特殊的函数对象，由特殊的语言结构——lambda表达式自动构建。C++闭包中保存了其代码内全部向外引用的变量的拷贝或引用。如果是对外界环境中的对象的引用，且闭包执行时该外界环境的变量已经不存在（如在调用栈上已经展开），那么可导致未定义行为，因为C++并不扩展这些被引用的外界环境的变量的生命期。示例代码如下：
<syntaxhighlight lang="cpp">
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' 现在是'n.end()'或指向'n'中第一个
 // 不等于'myname'且长度大于'y'的字符串
</syntaxhighlight>