`
Hooopo
  • 浏览: 335249 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

什么是闭包

    博客分类:
  • Ruby
阅读更多

via:http://www.ibm.com/developerworks/cn/linux/l-cn-closure/index.html

2008 年 7 月 10 日

随着硬件性能的提升以及编译技术和虚拟机技术的改进,一些曾被性能问题所限制的动态语言开始受到关注,Python、Ruby 和 Lua 等语言都开始在应用中崭露头角。动态语言因其方便快捷的开发方式成为很多人喜爱的编程语言,伴随动态语言的流行,我们经常听到一个名词——闭包,很多人会问闭包是什么?闭包是用来做什么的?本文汇集了有关闭包的概念、应用及其在一些编程语言中的表现形式,以供参考。

什么是闭包?

闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。对闭包的具体定义有很多种说法,这些说法大体可以分为两类:

  • 一种说法认为闭包是符合一定条件的函数,比如参考资源中这样定义闭包:闭包是在其词法上下文中引用了自由变量( 1)的函数。
  • 另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。比如参考资源中就有这样的的定义:在实现深约束( 2)时,需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。

这两种定义在某种意义上是对立的,一个认为闭包是函数,另一个认为闭包是函数和引用环境组成的整体。虽然有些咬文嚼字,但可以肯定第二种说法更确切。闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:

  • 函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。
  • 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

这些概念上的解释很难理解,显然一个实际的例子更能说明问题。Lua 语言的语法比较接近伪代码,我们来看一段 Lua 的代码:


清单 1. 闭包示例1
                
function make_counter()
 local count = 0

 function inc_count()
 count = count + 1
        return count

end
return inc_countendc1 = make_counter()c2 = make_counter()print(c1())print(c2())

在这段程序中,函数 inc_count 定义在函数 make_counter 内部,并作为 make_counter 的返回值。变量 count 不是 inc_count 内的局部变量,按照最内嵌套作用域的规则,inc_count 中的 count 引用的是外层函数中的局部变量 count。接下来的代码中两次调用 make_counter() ,并把返回值分别赋值给 c1 和 c2 ,然后又依次打印调用 c1 和 c2 所得到的返回值。

这里存在一个问题,当调用 make_counter 时,在其执行上下文中生成了局部变量 count 的实例,所以函数 inc_count 中的 count 引用的就是这个实例。但是 inc_count 并没有在此时被执行,而是作为返回值返回。当 make_counter 返回后,其执行上下文将失效,count 实例的生命周期也就结束了,在后面对 c1 和 c2 调用实际是对 inc_count 的调用,而此处并不在 count 的作用域中,这看起来是无法正确执行的。

上面的例子说明了把函数作为返回值时需要面对的问题。当把函数作为参数时,也存在相似的问题。下面的例子演示了把函数作为参数的情况。


清单 2. 闭包示例2
                
function do10times(fn)
 for i = 0,9 do
 fn(i)
 end
end

sum = 0
function addsum(i)
 sum = sum + i
end

do10times(addsum)
print(sum)


这里我们看到,函数 addsum 被传递给函数 do10times,被并在 do10times 中被调用10次。不难看出 addsum 实际的执行点在 do10times 内部,它要访问非局部变量 sum,而 do10times 并不在 sum 的作用域内。这看起来也是无法正常执行的。

这两种情况所面临的问题实质是相同的。在这样的语言中,如果按照作用域规则在执行时确定一个函数的引用环境,那么这个引用环境可能和函数定义时不同。要想使这两段程序正常执行,一个简单的办法是在函数定义时捕获当时的引用环境,并与函数代码组合成一个整体。当把这个整体当作函数调用时,先把其中的引用环境覆盖到当前的引用环境上,然后执行具体代码,并在调用结束后恢复原来的引用环境。这样就保证了函数定义和执行时的引用环境是相同的。这种由引用环境与函数代码组成的实体就是闭包。当然如果编译器或解释器能够确定一个函数在定义和运行时的引用环境是相同的( 3),那就没有必要把引用环境和代码组合起来了,这时只需要传递普通的函数就可以了。现在可以得出这样的结论:闭包不是函数,只是行为和函数相似,不是所有被传递的函数都需要转化为闭包,只有引用环境可能发生变化的函数才需要这样做。

再次观察上面两个例子会发现,代码中并没有通过名字来调用函数 inc_count 和 addsum,所以他们根本不需要名字。以第一段代码为例,它可以重写成下面这样:


清单 3. 闭包示例3
                
function make_counter()
 local count = 0

 return function()
 count = count + 1
 return count
 end
end

c1 = make_counter()
c2 = make_counter()

print(c1())
print(c2())


这里使用了匿名函数。使用匿名函数能使代码得到简化,同时我们也不必挖空心思地去给一个不需要名字的函数取名字了。

上面简单地介绍了闭包的原理,更多的闭包相关的概念和理论请参考参考资源中的"名字,作用域和约束"一章。

一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:

  • 函数是一阶值;
  • 函数可以嵌套定义;
  • 可以捕获引用环境,并
  • 把引用环境和函数代码组成一个可调用的实体;
  • 允许定义匿名函数;

这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。另外需要注意,有些语言使用与函数定义不同的语法来定义这种能被传递的"函数",如 Ruby 中的 Block。这实际上是语法糖,只是为了更容易定义匿名函数而已,本质上没有区别。

借用一个非常好的说法来做个总结( 4):对象是附有行为的数据,而闭包是附有数据的行为。





回页首


闭包的表现形式

虽然建立在相似的思想之上,各种语言所实现的闭包却有着不同的表现形式,下面我们来看一下闭包在一些常用语言中的表现形式。

JavaScript 中的闭包

JavaScript(ECMAScript)不是通用编程语言,但却拥有较大的用户群体,而 Ajax 的流行也使更多的人关注 JavaScript。虽然在进行 DOM 操作时容易引发循环引用问题,但 JavaScript 语言本身对闭包的支持还是很好的,下面是一个简单的例子:


清单 4. JavaScript
                
function addx(x) {
 return function(y) {return x+y;};
}

add8 = addx(8);
add9 = addx(9);

alert(add8(100));
alert(add9(100));


Ruby 中的闭包

随着 Ruby on Rails 的走红,Ruby 无疑是时下炙手可热的语言之一,Ruby 吸取了很多其他语言的优点,是非常优秀的语言,从这一点来看,很难说清是 Rails 成就了 Ruby 还是 Ruby 成就了 Rails。

Ruby 使用 Block 来定义闭包,Block 在 Ruby 中十分重要,几乎到处都可以看到它的身影,下面的代码就展示了一个 Block:


清单 5. Ruby
                
sum = 0
10.times{|n| sum += n}
print sum


10.times 表示调用对象10的 times 方法( 5),紧跟在这个调用后面的大括号里面的部分就是Block。所谓 Block 是指紧跟在函数调用之后用大括号或 do/end 括起来的代码,Block 的开始部分(左大括号或 do)必须和函数调用在同一行。Block 也可以接受参数,参数列表必须用两个竖杠括起来放在最前面。Block 会被作为它前面的函数调用的参数,而在这个函数中可以使用关键字 yield 来调用该 Block。在这个例子中,10.times 会以数字0到9为参数调用 Block 10次。

Block 实际上就是匿名函数,它可以被调用,可以捕获上下文。由于语法上要求 Block 必须出现在函数调用的后面,所以 Block 不能直接作为函数的的返回值。要想从一个函数中返回 Block,必须使用 proc 或 lambda 函数把 Block 转化为对象才行。详细内容请参考参考资源3

Python 中的闭包

Python 因其简单易学、功能强大而拥有很多拥护者,很多企业和组织在使用这种语言。Python 使用缩进来区分作用域的做法也十分有特点。下面是一个 Python 的例子:


清单 6. Python 1
                
def addx(x):
 def adder (y): return x + y
 return adder

add8 = addx(8)
add9 = addx(9)

print add8(100)
print add9(100)


在 Python 中使用 def 来定义函数时,是必须有名字的,要想使用匿名函数,则需要使用lambda 语句,象下面的代码这样:


清单 7. Python 2
                
def addx(x):
 return lambda y: x + y

add8 = addx(8)
add9 = addx(9)

print add8(100)
print add9(100)


Python 简单易用且功能强大,关于 Python 的更多信息请参考参考资源

Perl 中的闭包

Perl 是老牌文本处理语言了,在 WEB 开发方面也有一席之地。不过 Perl6 的开发进行比较慢,也许一些用户开始转投其它语言了。下面是一个 Perl 的例子。


清单 8. Perl
                
sub addx {
 my $x = shift;
 return sub { shift() + $x };
}

$add8 = addx(8);
$add9 = addx(9);

print $add8->(100);
print $add9->(100);


Lua 中的闭包

Lua 以其小巧和快速的特点受到游戏开发者的青睐,被一些游戏用来定制 UI 或作为插件语言,如果你玩过《魔兽世界》,那你对 Lua 一定不会感到陌生。前面在说明闭包原理时就使用了 Lua,这里就不再给出其他的例子了。更多的内容请参考参考资源

Scheme 中的闭包

Scheme 是 Lisp 的一种方言,被 MIT 用作教学语言。Scheme 属于函数语言,虽然不像命令语言那么流行,却是很多黑客喜欢的语言。很多编程思想起源于函数语言,闭包就是其中之一。一般认为 Scheme 是第一个提供完整闭包支持的语言。下面是一个 Scheme 的例子:


清单 9. Scheme
                
(define (addx x)
 (lambda (y) (+ y x)))

(define add8 (addx 8))
(define add9 (addx 9))

(add8 100)
(add9 100)


Scheme 的语法非常简单,只是有人觉得写法看起来比较古怪。有关 Scheme 更多信息请参考参考资源





回页首


闭包的应用

闭包可以用优雅的方式来处理一些棘手的问题,有些程序员声称没有闭包简直就活不下去了。这虽然有些夸张,却从侧面说明闭包有着强大的功能。下面列举了一些闭包应用。

加强模块化

闭包有益于模块化编程,它能以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。和没有使用闭包的程序相比,使用闭包可将模块划分得更小。比如我们要计算一个数组中所有数字的和,这只需要循环遍历数组,把遍历到的数字加起来就行了。如果现在要计算所有元素的积呢?要打印所有的元素呢?解决这些问题都要对数组进行遍历,如果是在不支持闭包的语言中,我们不得不一次又一次重复地写循环语句。而这在支持闭包的语言中是不必要的,比如对数组求和的操作在 Ruby 中可以这样做:


清单 10. 加强模块化
                
nums = [10,3,22,34,17]
sum = 0
nums.each{|n| sum += n}
print sum


这种处理方法多少有点像我们熟悉的回调函数,不过要比回调函数写法更简单,功能更强大。因为在闭包里引用环境是函数定义时的环境,所以在闭包里改变引用环境中变量的值,直接就可以反映到它定义时的上下文中,这是通常的回调函数所不能做到的。这个例子说明闭包可以使我们把模块划分得更小。

抽象

闭包是数据和行为的组合,这使得闭包具有较好抽象能力,下面的代码通过闭包来模拟面向对象编程。函数 make_stack 用来生成 stack 对象,它的返回值是一个闭包,这个闭包作为一个 Dispatcher,当以 “push” 或 “pop” 为参数调用时,返回一个与函数 push 或 pop 相关联的闭包,进而可以操作 data 中的数据。


清单 11. 抽象
                
function make_stack()

    local data = {};
    local last = -1;

    local function push(e)
        last = last + 1;
        data[last] = e;
    end

    local function pop()
        if last == -1 then
            return nil
        end
        last = last - 1
        return data[last+1]
    end

    return function (index)
        local tb = {push=push, pop=pop}
        return tb[index]
    end
end

s = make_stack()

s("push")("test0")
s("push")("test1")
s("push")("test2")
s("push")("test3")

print(s("pop")())
print(s("pop")())
print(s("pop")())


如果加入一些方便调用“对象方法”的语法糖,这看起来很像是面向对象的语法。当然 Lua 中有自己的面向对象语法和机制,所以几乎看不到有人写这样的 Lua 代码,但是对于 Scheme 等没有内建面向对象支持也没有内建复杂数据抽象机制的语言,使用闭包来进行抽象是非常重要的手段。

简化代码

我们来考虑一个常见的问题。在一个窗口上有一个按钮控件,当点击按钮时会产生事件,如果我们选择在按钮中处理这个事件,那就必须在按钮控件中保存处理这个事件时需要的各个对象的引用。另一种选择是把这个事件转发给父窗口,由父窗口来处理这个事件,或是使用监听者模式。无论哪种方式,编写代码都不太方便,甚至要借助一些工具来帮助生成事件处理的代码框架。用闭包来处理这个问题则比较方便,可以在生成按钮控件的同时就写下事件处理代码。比如在 Ruby 中可以这样写:


清单 12. 简化代码
                
song = Song.new
start_button = MyButton.new("Start") { song.play }
stop_button = MyButton.new("Stop") { song.stop }


更多

闭包的应用远不止这些,这里列举的只能算是冰山一角而已,并且更多的用法还不断发现中。要想了解更多的用法,多看一些代码应该是个不错的选择。





回页首


总结

闭包能优雅地解决很多问题,很多主流语言也顺应潮流,已经或将要引入闭包支持。相信闭包会成为更多人爱不释手的工具。闭包起源于函数语言,也许掌握一门函数语言是理解闭包的最佳途径,而且通过学习函数语言可以了解不同的编程思想,有益于写出更好的程序。





回页首


注解

  1. 自由变量是指除局部变量以外的变量。
  2. 英文原词是 binding,也有人把它翻译为绑定。
  3. 一个函数中没有自由变量时,引用环境不会发生变化。
  4. 出自 Python 社区。
  5. 在Ruby中一切都是对象,数字也是对象。

分享到:
评论
3 楼 RednaxelaFX 2009-05-07  
正好在别的地方跟别人讨论闭包,有兴趣的话来看看?http://www.keakon.cn/bbs/thread-1170-1-1.html
2 楼 Hooopo 2009-05-07  
RednaxelaFX 写道

嗯,这篇转得不错。不过在多数现实的编程语言里,自由变量不只是局部变量以外的变量,还得把全局变量也刨除掉,因为一般对全局变量的处理与对局部变量的不一样。而且还有个很有趣的问题,就是“变量”是否可变。像Haskell那样,变量不可变,那么闭包捕获的就只是值而已;而像Ruby、JavaScript这些语言,变量是可变的,则闭包捕获的就必须是引用而不能只是值。之前你提到Ruby的for循环的一个诡异问题就是因为闭包捕获了引用而造成的。

原来这样呀,3q!!
1 楼 RednaxelaFX 2009-05-05  
嗯,这篇转得不错。
不过在多数现实的编程语言里,自由变量不只是局部变量以外的变量,还得把全局变量也刨除掉,因为一般对全局变量的处理与对局部变量的不一样。
而且还有个很有趣的问题,就是“变量”是否可变。像Haskell那样,变量不可变,那么闭包捕获的就只是值而已;而像Ruby、JavaScript这些语言,变量是可变的,则闭包捕获的就必须是引用而不能只是值。之前你提到Ruby的for循环的一个诡异问题就是因为闭包捕获了引用而造成的。

相关推荐

    什么是闭包?闭包的优缺点?

    什么是闭包?闭包的优缺点?

    javascript里的闭包是什么 什么是闭包.zip

    JavaScript中的闭包是一种重要的编程概念,它涉及到函数、作用域和变量持久化等多个核心知识点。闭包的本质是函数能够访问并操作其外部作用域内的变量,即使在其外部作用域已经结束之后仍然能保持对这些变量的访问。...

    11.10 什么是闭包|Python的高级语法与用法|Python3.8入门 & 进阶 & 原生爬虫实战完全解读

    11.10_什么是闭包|Python的高级语法与用法|Python3.8入门_&_进阶_&_原生爬虫实战完全解读

    python高阶闭包练习题

    首先,让我们澄清一下什么是闭包。闭包是Python中一个内嵌函数,它可以访问并操作其外部函数的作用域内的变量,即使外部函数已经执行完毕。关键在于,当内嵌函数引用了外部函数的非局部变量,并且作为返回值或者赋值...

    javascript闭包详解

    #### 一、什么是闭包? 闭包是JavaScript中一个重要的概念,它涉及函数如何访问外部作用域中的变量。虽然官方定义较为复杂:“闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些...

    闭包javascript.pdf

    ### 什么是闭包? 在计算机科学中,闭包(Closure)是一种函数,它在一个包含一个或多个绑定变量的环境中被评估。当这个函数被调用时,它可以访问那些绑定变量。换句话说,闭包可以记住在其创建时周围存在的任何...

    javaScript闭包

    #### 什么是闭包 闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包最常见的方式就是在一个函数内部创建另一个函数,这样内部函数就可以访问外部函数的所有变量。这些变量被内部函数“记住”,即使外部函数...

    闭包与高阶函数案例解答

    什么是闭包 `当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。` 偏函数和柯里化的区别: 柯里化是把一个接受 n 个参数的函数,由原本的一次性传递所有参数并执行...

    Swift语言采用Closure闭包实现反向传值Demo

    首先,让我们了解什么是闭包。闭包是能够捕获和存储其所在上下文中自由变量的函数。自由变量是在闭包体中引用,但在闭包定义时未绑定到特定作用域的变量。在Swift中,闭包表达式提供了一种简洁的方式来写闭包,甚至...

    10-作用域链和闭包:代码中出现相同的变量,JavaScript引擎是如何选择的?_For_vip_user_0011

    然后,我们又学习了什么是闭包,并且了解了闭包在JavaScript中的应用。 下面,我们将详细地解释文章中的知识点: 首先,我们需要了解什么是作用域链。作用域链是指JavaScript引擎在查找变量时所经过的路径。它是由...

    顽皮的闭包

    1. **闭包的基本概念**:解释什么是闭包,它是如何形成的,以及它与作用域的关系。 2. **作用域链**:详细解释JavaScript的作用域链,如何通过作用域链访问变量,以及闭包如何维持对外部作用域的访问。 3. **变量...

    第九课 闭包-011

    接下来,我们讨论什么是闭包。官方定义中,闭包是由函数及其相关的引用环境组合而成的实体。简单来说,闭包就是一个函数,它可以访问并操作另一个函数的局部变量。这种特性使得闭包可以在函数内部创建函数,而内部...

    Java中的闭包与回调.rar

    首先,让我们来理解什么是闭包。在Java中,闭包是一种能够访问并操作其自身作用域内的变量,即使在其定义的外部也能访问,这种能力使得闭包成为一种强大的工具。闭包通常与匿名内部类关联,因为它们可以捕获和保存...

    005课-继承作用域闭包.rar

    3. **闭包的原理**:解释什么是闭包,如何创建闭包,以及闭包如何保持对外部变量的引用。 4. **实战应用**:可能包括使用继承和闭包实现模块化设计,或者通过闭包实现异步操作的封装。 5. **常见陷阱和最佳实践**...

    C语言实现三种闭包算法(传递,自反,对称闭包)

    在计算机科学中,闭包是一种重要的概念,尤其在函数式编程和逻辑编程中。闭包在C语言中可能不如在一些动态类型的语言中那么常见,但依然可以通过巧妙的技巧实现。这里我们将详细讨论如何用C语言实现传递闭包、自反...

    python闭包.html

    什么是闭包?如何设置闭包?创建闭包可以用来干什么?闭包的好处,如何正确使用闭包?通过闭包能创建一些只有当前函数能访问的变量,

    离散数学-关系,集合,求自反闭包,对称闭包,传递闭包

    离散数学-关系,集合,求自反闭包,对称闭包,传递闭包 离散数学-关系,集合,求自反闭包,对称闭包,传递闭包 离散数学-关系,集合,求自反闭包,对称闭包,传递闭包 离散数学-关系,集合,求自反闭包,对称闭包...

    JS闭包是是什么?

    1. **什么是闭包** 闭包的核心在于它可以访问并操作其定义时所在的作用域,而不仅仅是在调用时的作用域。它是由一个内部函数和该函数可以访问的外部变量(或称为自由变量)组成的。在上面的例子中,`function a()` ...

    关系闭包的计算

    ### 关系闭包的计算 #### 实验背景与目的 在计算机科学与数学领域中,关系闭包是一种重要的概念,特别是在图论与数据库理论中有着广泛的应用。本实验旨在通过编程实践的方式帮助学习者深入理解关系闭包的概念,并...

Global site tag (gtag.js) - Google Analytics