2007 年 1 月 22 日
闭包是可以用作函数参数和方法参数的代码块。一直以来,这种编程结构都是一些语言(如 Lisp、Smalltalk 和 Haskell)的重要组成部分。尽管一些颇具竞争力的语言(如 C#)采纳了闭包,但 Java 社区至今仍抵制对它的使用。本文探讨闭包在为编程语言带来一点点便利的同时是否也带来不必要的复杂性、闭包还有无更多的益处。
10 年前,我刚刚开始山地自行车运动的时候,我更愿意选用零件尽可能少尽可能简单的自行车。稍后,我意识到一些零件(如后减震器)可以保护我的背部和我自行车的框架在德克萨斯州高低起伏的山区中免受损害。我于是可以骑得更快,出问题的次数也渐少。虽然随之带来了操作上的复杂性和维护需求的增加,但对于我来说这点代价还是值得的。
|
关于闭包这个问题,Java 爱好者们现在陷入了类似的争论中。一些人认为闭包带给编程语言的额外复杂性并不划算。他们的论点是:为了闭包带来的一点点便利而打破原有语法糖的简洁性非常不值得。其他一些人则认为闭包将引发新一轮模式设计的潮流。要得到这个问题的最佳答案,您需要跨越边界,去了解程序员在其他语言中是如何使用闭包的。
Ruby 中的闭包
闭包是具有闭合作用域 的匿名函数。下面我会详细解释每个概念,但最好首先对这些概念进行一些简化。闭包可被视作一个遵循特别作用域规则且可以用作参数的代码块。我将使用 Ruby 来展示闭包的运行原理。用 irb 命令启动解释程序,然后用 load filename 命令加载每个样例。清单 1 是一个最简单的闭包:
3.times {puts "Inside the times method."} Results: Inside the times method. Inside the times method. Inside the times method. |
times
是作用在对象 3
上的一个方法。它执行三次闭包中的代码。{puts "Inside the times method."}
是闭包。它是一个匿名函数,times
方法被传递到该函数,函数的结果是打印出静态语句。这段代码比实现相同功能的 for
循环(如清单 2 所示)更加紧凑也更加简单:
for i in 1..3 puts "Inside the times method." end |
Ruby 添加到这个简单代码块的第一个扩展是一个参数列表。方法或函数可通过传入参数与闭包通信。在 Ruby 中,使用在 ||
字符之间用逗号隔开的参数列表来表示参数,例如 |argument, list|
。用这种方法使用参数,可以很容易地在数据结构(如数组)中构建迭代。清单 3 显示了在 Ruby 中对数组进行迭代的一个例子:
['lions', 'tigers', 'bears'].each {|item| puts item} Results: lions tigers bears |
each
方法用来迭代。您通常想要用执行结果生成一个新的集合。在 Ruby 中,这种方法被称为 collect
。您也许还想在数组的内容里添加一些任意字符串。清单 4 显示了这样的一个例子。这些仅仅是众多使用闭包进行迭代的方法中的两种。
animals = ['lions', 'tigers', 'bears'].collect {|item| item.upcase} puts animals.join(" and ") + " oh, my." LIONS and TIGERS and BEARS oh, my. |
在清单 4 中,第一行代码提取数组中的每个元素,并在此基础上调用闭包,然后用结果构建一个集合。第二行代码将所有元素串接成一个句子,并用 " and "
加以分隔。到目前为止,介绍的还都是语法糖而已。所有这些均适用于任何语言。
到目前为止看到的例子中,匿名函数都只不过是一个没有名称的函数,它被就地求值,基于定义它的位置来决定它的上下文。但如果含闭包的语言和不含闭包的语言间惟一的区别仅仅是一点语法上的简便 —— 即不需要声明函数 —— 那就不会有如此多的争论了。闭包的好处远不止是节省几行代码,它的使用模式也远不止是简单的迭代。
闭包的第二部分是闭合的作用域,我可以用另一个例子来很好地说明它。给定一组价格,我想要生成一个含有价格和它相应的税金的销售-税金表。我不想将税率硬编码到闭包里。我宁愿在别处设置税率。清单 5 是可能的一个实现:
tax = 0.08 prices = [4.45, 6.34, 3.78] tax_table = prices.collect {|price| {:price => price, :tax => price * tax}} tax_table.collect {|item| puts "Price: #{item[:price]} Tax: #{item[:tax]}"} Results: Price: 4.45 Tax: 0.356 Price: 6.34 Tax: 0.5072 Price: 3.78 Tax: 0.3024 |
在讨论作用域前,我要介绍两个 Ruby 术语。首先,symbol 是前置有冒号的一个标识符。可抽象地把 symbol 视为名称。:price
和 :tax
就是两个 symbol。其次,可以轻易地替换字符串中的变量值。第 6 行代码的 puts "Price: #{item[:price]} Tax: #{item[:tax]}"
就利用了这项技术。现在,回到作用域这个问题。
请看清单 5 中第 1 行和第 4 行代码。第 1 行代码为 tax
变量赋了一个值。第 4 行代码使用该变量来计算价格表的税金一栏。但此项用法是在一个闭包里进行的,所以这段代码实际上是在 collect
方法的上下文中执行的!现在您已经洞悉了闭包 这个术语。定义代码块的环境的名称空间和使用它的函数之间的作用域本质上是一个作用域:该作用域是闭合的。这是个基本特征。这个闭合的作用域是将闭包同调用函数和定义它的代码联系起来的纽带。
[1] [2] [3] 下一页