[翻译]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

那么何时使用块,何时使用过程呢?

  1. 块:你的方法把一个对象划分为多个部分,你想用户能够和这些部分交互。
  2. 块:你想原子的跑多个表达式,就像数据库迁移。
  3. Proc:重用块
  4. 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和方法对象表现的就像方法。通过以上的一些例子,你可以知道什么时候应该用哪种。

posted @ 2013-04-14 14:13  robbietree  阅读(180)  评论(0编辑  收藏  举报