理解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!

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。
整個調用如下:
1、一個整數組成的數組調用iterate!
2、當yield被調用時,把n(第一次爲1,第二次爲2,…)傳給block
3、block對n進行n**2。因爲是最後一行,自動作爲結果返回。
4、yield得到block的結果,並把值重寫到array裏。
5、數據中每個對象執行相同操作。

以上僅僅是個開始,yield只是調用block的一種方式,還有一種叫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]

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

def what_am_i(&block)
  block.class
end

puts what_am_i {}

# => Proc

block竟然是Proc!那Proc是什麼?

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只是一個普通類,不需要特殊處理。

上面的方式也是大多數語言處理閉包的方式。
而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呢?我一般這樣判斷:
1、Block:方法把一個對象拆分成很多片段,並且你希望你的用戶可以與這些片段做一些交互。
2、Block:希望自動運行多個語句,如數據庫遷移(database migration)。
3、Proc:希望多次複用一段代碼。
4、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個細微的區別。
1、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拋出了一個錯誤。
2、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/
翻譯很辛苦,轉載請附鏈接:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章