理解Ruby的4種閉包:blocks, Procs, lambdas 和 Methods。

blocks, Procs, Methods, lambdas(也稱閉包)是Ruby中最強大的一部分,用過你就會知道,同時也是最容易迷惑的。
這多是由於Ruby處理閉包的方式有點怪。更甚的是,Ruby有4種處理閉包的方式, 第一次用,每種都不太順手。 數據庫

首先:blocks代碼塊

最多見、最簡單、最富爭議、最有Ruby風格的方式是blocks。寫法以下: 數組

array = [1, 2, 3, 4]

array.collect! do |n|
  n ** 2
end

puts array.inspect

# => [1, 4, 9, 16]

do…end構成一個block。而後把這個block經過collect!傳給一個數組。就可使用block中的n來迭代數組中每一個元素。

collect!是Ruby庫裏的方法,下面咱們來寫一個本身的相似方法iterate! ruby

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!方法。方法名以!結尾表示危險方法,引發注意。如今咱們就可能像使用collect!同樣使用iterate! 閉包

與屬性不一樣,在方法中不須要指定block的名字,而是使用yield來調用。yield會執行block中的代碼。同時,注意咱們是怎麼把n(each_with_index當前處理的數字)傳給yield的。傳給yield的參數即對應了block中的參數(||中的部分)。如今n就能被block調用並在yield調用中返回n**2。
整個調用以下:
一、一個整數組成的數組調用iterate!
二、當yield被調用時,把n(第一次爲1,第二次爲2,…)傳給block
三、block對n進行n**2。由於是最後一行,自動做爲結果返回。
四、yield獲得block的結果,並把值重寫到array裏。
五、數據中每一個對象執行相同操做。 函數

以上僅僅是個開始,yield只是調用block的一種方式,還有一種叫Proc,看看。 spa

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]

和上一段代碼只有兩個不一樣
一、爲iterate!傳遞一個參數&code,&表示這個參數是block。
二、在iterate!中沒有使用yield而是call。
結果相同,爲何還要這種不一樣的語法呢?讓咱們先來看一個到底什麼是blocks吧? 翻譯

def what_am_i(&block)
  block.class
end

puts what_am_i {}

# => Proc

block居然是Proc!那Proc是什麼? code

Procs 過程

blocks很簡單,但當咱們須要處理不少blocks,屢次使用一個blocks時,咱們不得不重複代碼。既然Ruby是徹底面向對象的,咱們就能把這些可複用的代碼保存成object。這段可複用的代碼就是Proc(procedure的簡稱)
block與Proc唯一的不一樣是:block是不能保存的Proc,一性的。 對象

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]

注意:並無在 iterate!的參數頭部添加&,由於Proc只是一個普通類,不須要特殊處理。 three

上面的方式也是大多數語言處理閉包的方式。
而block是Ruby特有的方式。
另外Ruby不僅使用blocks作閉包還有一個緣由。好比有時咱們須要傳遞多個閉包給一個方法,這時block立刻力不從心了。但咱們能夠用Proc:

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:但願自動運行多個語句,如數據庫遷移(database migration)。
三、Proc:但願屢次複用一段代碼。
四、Proc:方法有一個或多個回調方法(callbacks)。

爲何block小寫,而Proc大寫

這只是我我的習慣。由於Proc是Ruby中的一個相似,而blocks並無本身的類(本質上只是Procs),只是一種語法規則。後面的lambda 小寫也是如此。

Lambda 拉姆達表達式

上面的Procs與blocks用法很像其它語言中的匿名函數(即lambdas)。Ruby也支持lambdas.

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]

lambdas看起來很像Procs,但它們有2個細微的區別。
一、lambdas檢查參數的個數,Procs不會。

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。但lambdas中,Ruby拋出了一個錯誤。
二、return不一樣。lambdas的return是返回值給方法,方法會繼續執行。Proc的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_return中,執行到Proc.new中的return時,直接返回」Proc.new」,不繼續執行。
lambda_return中,執行到lambda中的return時,返回」lambda」,方法繼續執行。

爲何會有這樣的不一樣?
答案在於procedures和methods概念上的不一樣。
Ruby中的Procs是代碼片斷(code snippets),不是方法。所以,Proc的return就是整個方法的return。
但lambdas就像是單獨的methods(只不過是匿名的),因此它要檢查參數個數,且不會覆蓋整個方法的返回。
所以,最好把lambdas看成另外一種methods的寫法,一種匿名的方式。

因此,何時用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

Ruby語法中通常參數(例子中爲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 })

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

使用lambda,代碼很天然。但若是用Proc,咱們須要對Arrays進行賦值。

Method 對象

當你想把一個方法以閉包的形式傳遞給另外一個方法,而且保持代碼DRY。你可使用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不是Proc,而是Method。Method與lambda用法相同,由於它們的概念是同樣的。不一樣的是Method是有名字的method,而lambda是匿名method.

總結

到此爲止,咱們已經瞭解了Ruby的4種閉包類型:blocks, Procs, lambdas 和 Methods。
blocks和Procs看起來像在代碼中插入代碼片斷。而lambdas和Methods看起來像方法。
經過幾個例子和比較,但願你能瞭解如何靈活運用閉包,遊刃有餘!

譯至:http://www.robertsosinski.com/2008/12/21/understanding-ruby-blocks-procs-and-lambdas/ 翻譯很辛苦,轉載請附連接:)

相關文章
相關標籤/搜索