[翻译]Understanding Ruby Blocks, Procs and Lambdas
Source:http://www.robertsosinski.com/2008/12/21/understanding-ruby-blocks-procs-and-lambdas/
块,过程, lambda(CS里称为闭包)是Ruby中最强大的方面之一,也是误解最多的方面之一。这可能是因为Ruby处理闭包的方式比较特别。更复杂的是Ruby有4种使用闭包的方式,每一种都有一点不同,但是区别又很微妙。
块
块是Ruby中使用闭包最Ruby化的方式。
array = [1, 2, 3, 4] array.collect! do |n| n ** 2 end puts array.inspect # => [1, 4, 9, 16]
解释如下:
1. 我们给一个数组发送collect!方法,并附带块。
2. 块对参数n做平方。
3. 现在数组里的元素是之前元素的平方值。
和collect!方法一起使用块是很简单的,我们只需要想collect!方法会作用在array的每个元素上。然而,如果我们要写我们自己的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!方法一样。
不像属性,你不需要给块命名。反而,你可以使用yield关键字。调用这个关键字会执行传递给方法的块。同时,注意怎么在yield中使用n。传递给yield的参数和block中在管道中声明的参数相对应。所以,回顾下发生了什么:
1. 给数组发送iterate!方法
2. 调用yield的时候,传递参数n,进而传递给块
3. 块给参数n平方,并返回
4. yield返回这个值,并赋值回原先的数组
5. 数组中的每个值都会进行3-4的处理
过程是使用代码块的另一种方式。
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]
和之前的例子很像,但是有两点不同。第一,有带&的参数,这个参数就是我们的块。第二,我们发送call方法给block而不是使用yield。结果一模一样。那么这两种语法有什么区别呢?
def what_am_i(&block) block.class end puts what_am_i {} # => Proc
可以看到块是一种过程,那么什么是过程呢?
使用块很方便语法也很简单,但是我们可能想多次使用块。但是,重复传递相同的块会使代码很难维护。然而,因为Ruby是面向对象的,这点可以通过生存对象来解决。使用过程的例子如下:
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)
过程大写是因为过程就是Ruby里的普通类。但是,块没有自己的类,它只是Ruby的一种语法。所以,块是小写的。lambda是小写的也是基于这个原因。
注意在我们的iterate!方法,code参数没有&。这个是因为传递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!(Proc.new do |n| n ** 2 end) puts array.inspect # => [1, 4, 9, 16]
以上是大多数语言中处理闭包的方式,这和发送块是一样的。但是,你可能会说这不是很Ruby化,我也同意这点。这也是为什么Ruby会有块。
那么为什么不仅仅保留块呢?答案很简单,如果要传递多个闭包,块的局限性就很大。但是如果使用过程的话,我们可以这样:
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
那么何时使用块,何时使用过程呢?
- 块:你的方法把一个对象划分为多个部分,你想用户能够和这些部分交互。
- 块:你想原子的跑多个表达式,就像数据库迁移。
- Proc:重用块
- Proc:可以有多个回调
Lambdas
到目前为止,你有两种方式使用过程,一种是作为参数传递,一种是保存为一个变量。这种方式很像其他语言的匿名函数,或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很像过程。但是有两点不同。第一点是不像过程,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)
过程中,多余的参数会被设置为nil。但是lambda中会抛错。
第二点不同是,lambda有小的返回。意思是在过程中如果有返回会直接返回出使用过程的方法,而lambda会返回值给方法,由方法本身返回。
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语句,就停止执行接下来的方法而直接返回。但是,在lambda_return方法中,就算lambda中有return,方法的余下部分还是会继续执行,直到返回。为什么有这个区别?
答案是过程和方法的概念性区别。Ruby中过程是以代码块的方式存在,而不是方法。所以,在过程中返回就是proc_return方法的返回。而 lambda表现的和方法一样,检查参数个数,并且不覆盖调用函数的返回。所以,最好是把lambda理解成是方法的另一种存在,匿名方法的方式。
所以,什么时候需要些匿名方法而不是过程呢?下面的代码演示了一个例子
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
Ruby的语法中参数不能有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 }) 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 4 and a # => Give me a 3 and a 4
这里,我们的generic_return方法期待闭包返回两个值。如果没有return关键字就会变得很恶心。通过lambda,事情就简单了。不过就算是过程,我们也可以利用Ruby的多值返回。
那么,什么时候使用过程,什么时候使用lambda呢?坦白的讲,除了参数检查外,区别仅仅是你怎么看待闭包。如果你比较适应块的方式,就使用过程。不过如果lambda就是方法的话,那我们可不可以保存现有的方法并像过程一样传递呢?
方法对象(Method Objects)
你已经有一个可以工作的方法,但是你想把它当做一个闭包传给另一个方法,但是又不想重复代码。这个时候,你可以利用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的方法。我们可以通过把它转换成Method对象传递给我们的iterate!方法。那么这个新的对象是什么类型呢?
def square(n) n ** 2 end puts method(:square).class # => Method
就像你猜到的,square不是一个过程,而是一个Method。漂亮的是这个对象工作起来和lambda一样,但是这里面的概念是相同的。然而,这就是有名字的方法而不是lambda所谓的匿名方法。
总结
重新回顾下,我们看过了Ruby的四种闭包方式,块,过程,lambda,和方法对象。我们也知道块和过程表现的就像代码块,而lambda和方法对象表现的就像方法。通过以上的一些例子,你可以知道什么时候应该用哪种。