闭包 (计算机科学)在计算机科学中,闭包(英語:Closure),又稱词法闭包(Lexical Closure)或函數閉包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。 概述闭包的概念出现于60年代,最早实现闭包的程序语言是Scheme。之后,闭包被广泛使用于函数式编程语言如ML语言和LISP。很多命令式程序语言也开始支持闭包。 在支持头等函数的语言中,如果函数f内定义了函数g,那么如果g存在自由变量,且这些自由变量没有在编译过程中被优化掉,那么将产生闭包。 闭包和匿名函数经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。如果从实现上来看的话,匿名函数如果没有捕捉自由变量,那么它其实可以被实现为一个函数指针,或者直接内联到调用点,如果它捕捉了自由变量那么它将是一个闭包;而闭包则意味着同时包括函数指针和环境两个关键元素。在编译优化当中,没有捕捉自由变量的闭包可以被优化成普通函数,这样就无需分配闭包结构体,这种编译技巧被称为函数跃升。 词源闭包的概念是在1960年代为采用lambda演算的表达式的机器求值而开发的,它首次在1970年于PAL编程语言中完全实现,用来支持词法作用域的头等函数[1]。 彼得·兰丁(Peter Landin)在1964年将术语“闭包”定义为一种包含环境成分和控制成分的实体,用于在他的SECD机器上对表达式求值[2]。 Joel Moses认为是Landin发明了“闭包”这一术语,用来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的lambda表达式,从而形成了闭合的表达式,或称闭包。[3][4]。这一用法后来于1975年被Sussman和Steele在定义 Scheme语言的时候予以采纳[5],并广为流传。 语义闭包和状态表达闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。 不过,用这种方式来使用闭包时,闭包不再具有参照透明性,因此也不再是纯函数。即便如此,在某些非纯函数式编程语言,例如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表达式 这个闭包接着被传递到 下面是用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; }
);
}
这里,关键字 一个函数可以创建一个闭包并返回它,如下述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;
};
}
因为在这个例子中闭包已经超出了创建它的函数的范围,所以变量 闭包的用途
(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"
闭包的实现典型实现方式是定义一个特殊的数据结构,保存了函数地址指针与闭包创建时的函数的词法环境表示(那些非局部变量的绑定)。使用函数调用栈的语言实现闭包比较困难,因而这也说明了为什么大多数实现闭包的语言是基于垃圾收集机制——当然,不使用垃圾收集也可以做到。 闭包的实现与函数对象很相似。 通过将自由变量放进参数表、并扩大函数名字的作用域,可以把一个闭包 / 匿名 / 内部函数变成一个普通的函数,这叫做“Lambda 提升”。例: void G(void){
const std::wstring wstr=L"Hello, world!";
std::function<wchar_t(size_t)> fn=[&wstr](size_t ui)->wchar_t{
return wstr[ui%wstr.length()];
};
std::wcout<<fn(3)<<std::endl;//'l'
}
//那么 fn 是一个闭包,指向那个匿名函数。
//这里 wstr 是自由变量,首先将其放入参数表:
void G(void){
const std::wstring wstr=L"Hello, world!";
std::function<wchar_t(size_t, const std::wstring &)> fn=[](size_t ui, const std::wstring &wstr)->wchar_t{
return wstr[ui%wstr.length()];
};
std::wcout<<fn(3, wstr)<<std::endl;//'l'
}
//现在 fn 中没有自由变量了。把这个匿名函数取个名之后放到全局命名空间里:
wchar_t fn(size_t ui, const std::wstring &wstr){
return wstr[ui%wstr.length()];
}
void G(void){
const std::wstring wstr=L"Hello, world!";
std::wcout<<fn(3, wstr)<<std::endl;//'l'
}
//这就把 fn“提升”成了一个普通的函数。
各种语言中(类似)闭包的结构C语言的回调函数在C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的 gcc对C语言的扩展C语言扩展:BlocksC语言 (使用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++早期标准允许通过重载 C++11标准已经支持了闭包,这是一种特殊的函数对象,由特殊的语言结构——lambda表达式自动构建。C++闭包中保存了其代码内全部向外引用的变量的拷贝或引用。如果是对外界环境中的对象的引用,且闭包执行时该外界环境的变量已经不存在(如在调用栈上已经展开),那么可导致未定义行为,因为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' 现在是'n.end()'或指向'n'中第一个
// 不等于'myname'且长度大于'y'的字符串
}
参考文献
外部链接
|