[翻译]理解Ruby中的blocks,Procs和lambda
原文出处:Understanding Ruby Blocks, Procs and Lambdas
blocks,Procs和lambda(在编程领域被称为闭包)是Ruby中很强大的特性,也是最容易引起误解的特性。
这有可能是因为Ruby使用相当独特的方式来处理闭包。Ruby有四种处理闭包的方式,每一种方式都稍有点不同,甚至有点荒诞,这使得事情变得有点复杂。有不少网站提供了一些关于Ruby闭包的工作方式,但是我还没有找到一个非常有效的指南,希望本篇文章会成为这样的一篇指南。
一、首先来说blocks
最普遍,最简单也最没有争议的Ruby解决闭包的方式就是blocks,使用下面的语法:
- array = [1, 2, 3, 4]
- array.collect! do |n|
- n ** 2
- end
- puts array.inspect
- # => [1, 4, 9, 16]
那么,这个代码描述了什么呢?
1、我们将collect!方法和一个代码块发送在了一个Array上。(译者注:有的语言中将方法调用描述为在某个对象上发送了一个消息)
2、在collect!方法中代码块通过变量跟其进行交互(在本例中是n),并且对变量n求了平方。
3、接下来数组中的每个元素都被求了平方。
在collect!方法中使用代码块是很简单的,我们只需要设想collect!方法将会使代码块作用在数组的每个元素上即可。然而,如果我们想自己写一个类似于collect!的方法会是怎么样呢?我们写一个叫iterate!的方法看看。
- class Array
- def iterate!
- self.each_with_index do |n, i|
- self[i] = yield(n)
- end
- end
- end
- array = [1, 2, 3, 4]
- array.iterate! do |n|
- n ** 2
- end
- puts array.inspect
- # => [1, 4, 9, 16]
我们重新打开了Array类并且把我们自己的iterate!方法放在了里面。我们要遵守Ruby中的约定并在方法名后面放置一个!符号,从而提醒调用者,因为这个方法有可能会带来危险!(译者注:由于iterate方法修改了数组本身)然后我们可以像使用collect!方法一样使用iterate!方法。
代码块的问题在于,你无法给他指定一个明确的名称,从而可以在iterate!方法里面调用。相反,你通过在方法里面调用yield关键字将会执行代码块中的代码。另外,请注意我们如何将n传递给了yield关键字,传递给yield中的n对应了代码块管道列表中的变量。让我们概括一下发生的事情:
1、把iterate!方法扩展到了数组中。
2、当yield被调用时,数字n(n第一次是1,第二次是2,等等)将会被传递给代码块。
3、代码块中将会把n取平方,然后返回。
4、Yield输出了代码块中的值,并且重写了数组中的元素。
5、数组中的每一个元素都会执行这个过程。
目前我们有一个灵活的方式对代码块和方法之间交互。设想代码块是一个API,你可以通过代码块来对数组中的元素求平方、求立方、转化为字符串打印在屏幕等。这些无限的假设使得你的方法非常灵活和强大。
然而,这只是开始,在方法中使用yield关键字是使用代码块的其中一种方法,还有另外一个方式被称作Proc,让我们来看看。
- class Array
- def iterate!(&code)
- self.each_with_index do |n, i|
- self[i] = code.call(n)
- end
- end
- end
- array = [1, 2, 3, 4]
- array.iterate! do |n|
- n ** 2
- end
- puts array.inspect
- # => [1, 4, 9, 16]
这似乎跟前面的例子很相像,但是有两个区别。第一,我们传递了一个用&符号标记的参数&code。第二,在iterate!方法中,我们通过&code发送call方法而不是yield关键字来调用方法块。这两个实例的结果都是一样的,但是如果是这样,为什么我们需要不同的语法呢?先让我们看看blocks究竟是什么:
- def what_am_i(&block)
- block.class
- end
- puts what_am_i {}
- # => Proc
block是一个Proc!话虽如此,可是Proc又是什么?
二、Procs
Blocks使用起来非常方便和简单,然而如果有这样的一个需求:同一个blocks要被使用多次。多次使用同一个blocks将会违反DRY原则,Ruby作为一门面向对象的语言,他可以用相当简洁的方式将可重用的代码保存在一个对象中,这种可重用的代码块被称为Proc(procedure的简写),blocks和Proc之间唯一的区别是blocks是一个不能被保存的Proc,因此,他是一种一次性的解决方案。通过使用Procs,我们可以这样做:
- class Array
- def iterate!(code)
- self.each_with_index do |n, i|
- self[i] = code.call(n)
- end
- end
- end
- array_1 = [1, 2, 3, 4]
- array_2 = [2, 3, 4, 5]
- square = Proc.new do |n|
- n ** 2
- end
- array_1.iterate!(square)
- array_2.iterate!(square)
- puts array_1.inspect
- puts array_2.inspect
- # => [1, 4, 9, 16]
- # => [4, 9, 16, 25]
为什么要小写block,大写Proc?
我总是将Proc以大写开头是因为它是Ruby中一个类。然而,blocks没有自己的类,他仅仅是ruby中的一个语法。因此我会把block以小写开头,在接下来的教程中,我们将会使用以小写开头的lambda,我这样做是出于相同的原因。
请注意我们为何没在iterate!中使用带&符号的code参数?这是因为使用Procs和使用其他数据类型没有什么不同,当我们把Procs当作其他数据类型一样,我们让Ruby解释器做出一些有意思的事情。试一试:
- class Array
- def iterate!(code)
- self.each_with_index do |n, i|
- self[i] = code.call(n)
- end
- end
- end
- array = [1, 2, 3, 4]
- array.iterate!(Proc.new do |n|
- n ** 2
- end)
- puts array.inspect
- # => [1, 4, 9, 16]
以上是大多数语言处理闭包的方式,并且这种方式等同于发送一个代码块。也许你会说这不是Ruby风格,我会同意你的说法,因为这正是Ruby要引入blocks的原因之一。
如果是这样,为什么不能完全使用blocks呢?原因很简单,如果我们想传递两个闭包该怎么做?blocks变得过于有限,通过Procs我们可以这样:
- def callbacks(procs)
- procs[:starting].call
- puts "Still going"
- procs[:finishing].call
- end
- callbacks(:starting => Proc.new { puts "Starting" },
- :finishing => Proc.new { puts "Finishing" })
- # => Starting
- # => Still going
- # => Finishing
因此,什么时候使用blocks而不是Procs?我的逻辑如下:
block:你的方法把对象分解为更小的片段,并且你想要跟这些代码片段交互。
block:你想要以原子方式运行多个表达式,像数据库迁移。
Proc:你想多次重用同一个代码块
Proc:你的方法有超过一个的回调
三、Lambda
到目前为止,你已经用两种方式使用了Procs,直接传递代码块和当作一个变量来传递。这种Procs的使用方式跟其他语言中的匿名方法,lambda有点类似。Ruby中也可以直接使用lambda,让我们来看看:
- class Array
- def iterate!(code)
- self.each_with_index do |n, i|
- self[i] = code.call(n)
- end
- end
- end
- array = [1, 2, 3, 4]
- array.iterate!(lambda { |n| n ** 2 })
- puts array.inspect
- # => [1, 4, 9, 16]
初次看上去,lambda似乎跟Procs是一样的。然而,他们有两个细微的差别。第一,不像Procs,lambda会检查所传递的参数:
- def args(code)
- one, two = 1, 2
- code.call(one, two)
- end
- args(Proc.new{|a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}"})
- args(lambda{|a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}"})
- # => Give me a 1 and a 2 and a NilClass
- # *.rb:8: ArgumentError: wrong number of arguments (2 for 3) (ArgumentError)
在Proc的例子中,多余的变量为设置为了nil,然而在lambda中,Ruby将会抛出一个错误。
第二个不同是:在Procs遇到return将会终止方法并返回值,lambda中遇到return将不会终止代码,是不是有点混乱?让我们看一个例子:
- def proc_return
- Proc.new { return "Proc.new"}.call
- return "proc_return method finished"
- end
- def lambda_return
- lambda { return "lambda" }.call
- return "lambda_return method finished"
- end
- puts proc_return
- puts lambda_return
- # => Proc.new
- # => lambda_return method finished
在proc_return中,我们的方法被return关键字打断了,剩下的方法被终止了并输出了字符串Proc.new。另一方面,在lambda_return中返回了字符串并继续执行了剩下的语句。为什么会有这样的不同?
答案是:过程(procedure)和方法(method)的概念差异。Procs在Ruby中是代码片段,并不是方法。而我们可以将lambda看作是一种方法的编写方式,你可以理解为匿名方法。
什么时候使用匿名方法(lambda)来替换Proc呢?看看接下来的例子:
- def generic_return(code)
- code.call
- return "generic_return method finished"
- end
- puts generic_return(Proc.new { return "Proc.new" })
- puts generic_return(lambda { return "lambda" })
- # => *.rb:6: unexpected return (LocalJumpError)
- # => generic_return method finished
在Proc的使用中,参数不能有return关键字。然而在lambda中,可以写return并可以正确执行。这种不同的语义形式表现在类似的实例中:
- def generic_return(code)
- one, two = 1, 2
- three, four = code.call(one, two)
- return "Give me a #{three} and a #{four}"
- end
- puts generic_return(lambda { |x, y| return x + 2, y + 2 })
- puts generic_return(Proc.new { |x, y| return x + 2, y + 2 })
- puts generic_return(Proc.new { |x, y| [x + 2, y + 2] })
- # => Give me a 3 and a 4
- # => *.rb:9: unexpected return (LocalJumpError)
- # => Give me a 3 and a 4
在这里,方法generic_return期望闭包返回两个值。如果要实现这个需求没有return关键字显得不够那么清晰,在lambda中一切都很正常,但是在Proc中就会报错。
所以何时使用Proc和lambda?老实说,除了参数检查,不同的只是你如何看待闭包。如果你觉得传递的是代码块,请使用Proc;如果你觉得你将一个方法传递到了另一个方法中,lambda对你更有意义。如果将lambda看作是方法,如何将已经存在的方法传递给另一个方法呢?
四、Method对象
目前,你已经有一个正常工作的方法,但是你想把此方法当作闭包传递个另一个方法中,为了达到这个目的,你可以使用Ruby提供的method方法。
- class Array
- def iterate!(code)
- self.each_with_index do |n, i|
- self[i] = code.call(n)
- end
- end
- end
- def square(n)
- n ** 2
- end
- array = [1, 2, 3, 4]
- array.iterate!(method(:square))
- puts array.inspect
- # => [1, 4, 9, 16]
在这个例子中,我们已经有一个叫做square的方法,我们可以把它转化为一个方法对象并且传递给iterate!,这个method对象是什么类型呢?
- def square(n)
- n ** 2
- end
- puts method(:square).class
- # => Method
正如你所见,square不是一个Proc类型,而是一个Method类型。有趣的是这个Method对象更像是一个lambda,因为从概念上可以发现他俩的表现一样。只是这个方法有一个名称叫做square,而lambda是匿名方法。
五、结论
截至目前,我们看到了Ruby中使用闭包的四种方式:blocks,Procs,lambda和Method。同时我们还知道了blocks和Procs是代码块,而lambda和Method是方法。通过本文的实例,能帮你在不同的场景选择有效的使用方法。现在可以向你的小伙伴展示ruby灵活的特性了。
[翻译]理解Ruby中的blocks,Procs和lambda的更多相关文章
- 理解Ruby中的作用域
作用域对于Ruby以及其它编程语言都是一个需要理解的至关重要的基础知识.在我刚开始学习ruby的时候遇到很多诸如变量未定义.变量没有正确赋值之类的问题,归根结底是因为自己对于ruby作用域的了解不够, ...
- [翻译]理解Swift中的Optional
原文出处:Understanding Optionals in Swift 苹果新的Swift编程语言带来了一些新的技巧,能使软件开发比以往更方便.更安全.然而,一个很有力的特性Optional,在你 ...
- 深入理解CSS中的层叠上下文和层叠顺序(转)
by zhangxinxu from http://www.zhangxinxu.com 本文地址:http://www.zhangxinxu.com/wordpress/?p=5115 零.世间的道 ...
- Ruby中Block, Proc, 和Lambda
Block Blocks就是存放一些可以被执行的代码的块,通常用do...end 或者 {}表示 例如: [1, 2, 3].each do |num| puts num end [1, 2, 3]. ...
- 深入理解CSS中的层叠上下文和层叠顺序
零.世间的道理都是想通的 在这个世界上,凡事都有个先后顺序,凡物都有个论资排辈.比方说食堂排队打饭,对吧,讲求先到先得,总不可能一拥而上.再比如说话语权,老婆的话永远是对的,领导的话永远是对的. 在C ...
- ruby中symbol
Symbol 是什么 Ruby 是一个强大的面向对象脚本语言(本文所用 Ruby 版本为1.8.6),在 Ruby 中 Symbol 表示“名字”,比如字符串的名字,标识符的名字. 创建一个 Symb ...
- 转:理解 PHP 中的 Streams
本文转自:开源中国社区 [http://www.oschina.net]本文标题:理解 PHP 中的 Streams 本文地址:http://www.oschina.net/translate/und ...
- [Ruby学习总结]Ruby中的类
1.类名的定义以大写字母开头,单词首字母大写,不用"_"分隔 2.实例化对象的时候调用new方法,实际上调用的是类里边的initialize方法,是ruby类的初始化方法,功能等同 ...
- 【Apache ZooKeeper】理解ZooKeeper中的ZNodes
理解ZooKeeper中的ZNodes 翻译自:http://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html ZooKeeper中的 ...
随机推荐
- [python] 创建临时文件-tempfile模块
This module generates temporary files and directories. It works on all supported platforms.In versio ...
- 为什么不能访问django自带的索引页
通过HTTP://192.168.160.128:8000访问虚拟机上的django索引页出现“ 无法访问此网站 192.168.160.128 拒绝了我们的连接请求. ” 是什么原因呢?费了好大一番 ...
- ms sql server 在cmd中执行sqlcmd的时候报错
cmd下直接输入sqlcmd会提示 错误: HResult 0x2,级别 16,状态 1命名管道提供程序: 无法打开与 SQL Server 的连接 [2].Sqlcmd: 错误: Microsoft ...
- aix 维护常用命令
errpt - dH :如果有记录表示硬故障件出现.#向ibm报修 mail:关键错误信息会以mail方式发给root用户.#根据报错程序联系相应厂家. df -g: 文件系统不可以,当/va ...
- android源码环境下用mmm/mm编译模块,输出编译log到文件的方法
android源码环境下用mmm/mm编译模块,输出编译log到文件的方法 1,在android目录下直接用mmm命令编译, log信息保存在android目录下 mmm packages/apps/ ...
- Android照片墙加强版,使用ViewPager实现画廊效果
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/12646775 记得关于照片墙的文章我已经写过好几篇了,有最基本的照片墙,有瀑布流模 ...
- git 命令大全
git init # 初始化本地git仓库(创建新仓库) git config --global user.name "xxx" # 配置用户名 git config --glob ...
- <数据结构与算法>之字符串,散列,布隆过滤器。
1:字符串 字符串是一组由数字,字符,下划线的一串字符,是特殊的一维数组. 2:字符串的应用 字符串移位包含问题: 例:给定两个字符串s1和s2,要求判断s2是否能被s1做循环移位得到字符串包含.例如 ...
- 利用JDBC连接MySQL并使用MySQL
driver为JDBC的驱动. url为数据库的地址. usrname和password分别为数据库的用户名和密码. Connection类用来连接MySQL. PreparedStatement类用 ...
- 编译gtk+程序报错gcc: pkg-config --cflags --libs gtk+-2.0: 没有那个文件或目录
第一次接触gtk+.在网上搜罗良一番,装好相应的库后,编写了第一hello程序.在编译时输入以下命令:gcc -o hello hello.c 'pkg-config --cflags --libs ...