闭包 (计算机科学)

维基百科,自由的百科全书
跳转至: 导航搜索

计算机科学中,闭包Closure)是词法闭包Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

闭包的概念出现于60年代,最早实现闭包的程序语言是Scheme。之后,闭包被广泛使用于函数式编程语言如ML语言LISP。很多命令式程序语言也开始支持闭包。

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。

闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。

词源[编辑]

Peter J. Landin 在1964年将术语 闭包 定义为一种包含 环境成分控制成分的实体,用于在他的SECD 机器上对表达式求值。[1] Joel Moses 认为是 Landin 发明了 闭包 这一术语,用来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的 lambda 表达式,从而形成了 闭合的表达式,或称闭包。[2][3] 这一用法后来于 1975 年被 SussmanSteele 在定义 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取代了lambdaArray.filter方法[5]取代了filter函数,但两段代码的功能是一样的。

一个函数可以创建一个闭包并返回它,如下述javascript例子:

// 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;
  };
}

因为在这个例子中闭包已经超出了创建它的函数的范围,所以变量fdx将在函数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"

闭包的实现[编辑]

典型实现方式是定义一个特殊的数据结构,保存了函数地址指针与闭包创建时的函数的词法环境表示(那些nonlocal变量的绑定)。使用函数调用栈的语言实现闭包比较困难,因而这也说明了为什么大多数实现闭包的语言是基于垃圾收集机制。

闭包的实现与函数对象很相似。这种技术也叫做lambda lifting

各种语言中(类似)闭包的结构[编辑]

C语言的回调函数[编辑]

C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的void*指针用以保存用户数据。这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。

gcc对C语言的扩展[编辑]

gcc编译器对C语言实现了一种闭包的程序特性。

C语言扩展:Blocks[编辑]

C语言 (使用LLVM编译器或苹果修改版的GCC)支持。闭包变量用__block标记。同时,这个扩展也可以应用到Objective-CC++中。

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++11标准已经支持了闭包,这是一种特殊的函数对象,由特殊的语言结构——lambda表达式自动构建。C++闭包中保存了全部nonlocal变量的拷贝或引用。如果是对外界环境中的对象的引用,且闭包执行时该外界环境的变量已经不存在(如在调用栈上已经unwinding),那么可导致undefined behavior,因为C++并不扩展这些被引用的外界环境的变量的生命期。示例代码如下

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'的字符串
}

参考资料[编辑]

  1. ^ P. J. Landin, The mechanical evaluation of expressions. 1964 
  2. ^ Joel Moses, The Function of FUNCTION in LISP, or Why the FUNARG Problem Should Be Called the Environment Problem (PDF). June 1970 [2009-10-27], AI Memo 199, "A useful metaphor for the difference between FUNCTION and QUOTE in LISP is to think of QUOTE as a porous or an open covering of the function since free variables escape to the current environment. FUNCTION acts as a closed or nonporous covering (hence the term "closure" used by Landin). Thus we talk of "open" Lambda expressions (functions in LISP are usually Lambda expressions) and "closed" Lambda expressions. [...] My interest in the environment problem began while Landin, who had a deep understanding of the problem, visited MIT during 1966-67. I then realized the correspondence between the FUNARG lists which are the results of the evaluation of "closed" Lambda expressions in LISP and ISWIM's Lambda Closures." 
  3. ^ Åke Wikström. Functional Programming using Standard ML. 1987. ISBN 0-13-331968-7. "The reason it is called a "closure" is that an expression containing free variables is called an "open" expression, and by associating to it the bindings of its free variables, you close it." 
  4. ^ Gerald Jay Sussman and Guy L. Steele, Jr., Scheme: An Interpreter for the Extended Lambda Calculus. December 1975, AI Memo 349 
  5. ^ array.filter. Mozilla Developer Center. 10 January 2010 [2010-02-09]. 
  6. ^ Re: FP, OO and relations. Does anyone trump the others?. 29 December 1999 [2008-12-23]. 

外部链接[编辑]