Skip to content

block、proc、yield、lambda之間的關係

NickWarm edited this page Mar 4, 2017 · 4 revisions

通常我們要定義一個method時會寫

def say_hello
  puts 'hello'
end

Functions stand alone and methods are members of a class.

A method is a piece of code that is called by a name that is associated with an object.

ref:

有時,我們會想要給method傳參數進去

def multiply(x)
  puts x * 2
end
pry(main)> multiply(2)
=> 4

proc 與 block

當傳參數已經無法滿足我了,我想要讓method使用一段程式碼(block)時,method的參數加上&

def myfun(&p)
  p.call
end

block是可以暫存一段ruby code的地方,如果只有一行可以用{...},若是很多行可以用do...end

block不是物件(object),沒有辦法單獨的存在,也沒辦法把它指定給某個變數

block也不是參數(variable),block通常得像寄生蟲一樣依附或寄生在其它的方法或物件(或是使用某些類別把它物件化)

ref

&就像是一個聲明,說得是我要傳一段block進來,然後我用call調用傳進來的block

如此一來,我就能在我的method後面,接一個block把這個block給物件化

如此一來,method裡的p就是一個物件,可以調用call這方法,來取得block的內容

pry(main)> myfun { puts "Hello world" }
=> Hello world

pry(main)> myfun { puts 2*2 }
=> 4

當參數使用&,如果直接呼叫myfun後面沒給block就會噴錯,因為myfun裡的call是一個method,method的存在是因為有著一個object來呼叫它。

pry(main)> myfun()
=> NoMethodError: undefined method `call' for nil:NilClass

pry(main)> myfun
=> NoMethodError: undefined method `call' for nil:NilClass

pry(main)> myfun() {}
=> nil

proc的使用時機

但是block的缺點是,他沒辦法把這段程式碼存起來,只有要用時放在method後面。

如果有 想要重複使用的程式碼,就用Proc包起來Proc也就是Procedure的意思。

使用Proc就可以把block物件化

# 原本用block的寫法
myfun { puts "use myproc" }

# 改用Proc寫

myproc = Proc.new { puts "use myproc" }

# 等同於

myproc = proc { puts "use myproc" }

Proc的default method就是call,讓我們能呼叫要重複使用的程式碼


[40] pry(main)> myproc2 = proc { |x| x *2 }
=> #<Proc:0x007fde7c666d60@(pry):66>

[41] pry(main)> myproc2.call(2)
=> 4

[42] pry(main)> myproc2.call(3)
=> 6

惱人的&

當method的參數使用&,後面又接Proc

# method
def myfun(&p)
  p.call
end

myproc = proc { puts "use myproc" }

執行程式時

pry(main)> myfun(&myproc)
=> use myproc

# 由於Ruby可以省略括號,所以也可以寫成

pry(main)> myfun &myproc
=> use myproc

# 由於我們在myfun這method有用 & 來定義進來的參數,所以myfun這method後面只能接block,
# 如果 myfun 後面接 Proc 生成的 object,則會噴錯

pry(main)> myfun myproc
=> ArgumentError: wrong number of arguments (1 for 0)

如果說,我在method的參數,不用&則會怎樣呢?

def myfun2(p)
  p.call
end

myproc = Proc.new { puts "use myproc" }

pry(main)> myfun2 myproc   # 這就是一般的 Proc 調用 call 方法
=> use myproc


pry(main)> myfun2 &myproc  # 使用 & 就是在聲明使用 block,若是使用block,則 myfun2的參數就該定義 &,由於myfun2的參數沒定義 & 所以噴錯
=> ArgumentError: wrong number of arguments (0 for 1)

如果自定義的method,它的參數不用&,而method裡又使用了call。則必定要傳Proc進去。因為callProc物件的default method

一個重要的小結論:

當method的定義裡面有用到call時,method的參數(變數)p

  • 加上&&p代表method後面要接則block,透過&p使block物件化,當p.call調用時,就會執行block
  • 沒有&p代表要傳proc這個object進到method裡去,當p.call調用時,就會執行傳進來的proc

yield

當我們想要傳block進入method時,會寫

def myfun(&p)
  p.call
end

如果想要省略掉&call,則我們可以使用yield,寫成

def myfun3
  yield
end

換句話說

# 原始寫法

def myfun(&p)
  p.call
end

# 等同於

def myfun3
  yield
end

使用yield

# 能傳block進來
pry(main)> myfun3 { puts "Hello world" }
=> Hello world

# 傳proc就會噴錯
myproc = Proc.new { puts "use myproc" }

pry(main)>myfun3 myproc
=> ArgumentError: wrong number of arguments (1 for 0)

小結:yield = 方便執行block的方式。yield語句同等於省略&block輸入 以及 block.call

lambda

要讓method使用一段程式碼,除了用blockProc之外,也能用lambda

lambda的寫法很簡單,例如:

lambda1 = lambda {|x| x * 2}

# 等同於

lambda2 = ->(x) { return x * 2 }

# 等同於

lambda3 = ->(x) { x * 2 }  # Ruby可以省略return

我們先看一下lambdaProc的關係

pry(main)> lambda2.class
=> Proc

可以看到,lambda的class就是Proc。由於lambdaProc,所以lambda能調用Proc的default method也就是call

pry(main)> lambda1.call("hello world")
=> "hello worldhello world"

pry(main)> lambda2.call("hello world")
=> "hello worldhello world"

pry(main)> lambda3.call("hello world")
=> "hello worldhello world"

proc與lambda的差異

lambdaproc的主要差別有兩個

  1. lambda會check參數數量,proc不會
  2. return的處理不同,lambda的return只會跳出lambda,proc的return會跳出整個method

lambda會check參數數量,proc不會

假如我定義lambda時只有一個參數,然後丟兩個參數進去就會噴錯

[3] pry(main)> lam2 = ->(x) {puts  x*2}
=> #<Proc:0x007fde7c5d6c60@(pry):3 (lambda)>

[4] pry(main)> lam2.call(3)
6
=> nil

[5] pry(main)> lam2.call(3,4)
ArgumentError: wrong number of arguments (2 for 1)

但若是Proc,只定義一個參數丟兩個參數進去,則會只取第一個參數

[6] pry(main)> proc1 = proc {|x| puts x * 2}
=> #<Proc:0x007fde7c54b610@(pry):6>

[7] pry(main)> proc1.call(2,3,4)
4
=> nil

換句話說,lambda對參數的確認較為嚴謹

return的處理不同

lambda的return只會跳出lambda

#proc and lambda
def run_a_proc(p)
  puts 'start...'
  p.call
  puts 'end.'
end

#the lambda will be ignore
def run_couple
  run_a_proc lambda { puts 'I am a lambda'; return }
  run_a_proc proc { puts 'I am a proc'; return }
end

pry(main)> run_couple
=> start...
=> I am a lambda
=> end.
=> start...
=> I am a proc
=> nil

proc的return會跳出整個method

#proc and lambda
def run_a_proc(p)
  puts 'start...'
  p.call
  puts 'end.'
end

#the lambda will be ignore
def run_couple2
  run_a_proc proc { puts 'I am a proc'; return }
  run_a_proc lambda { puts 'I am a lambda'; return }
end

pry(main)> run_couple2
=> start...
=> I am a proc
=> nil

block、Proc、lambda的小整理

# 使用yield取代 & 與 call
def f1
  yield
end

f1 { puts "f1" }


# 使用block
def f2(&p)
  p.call
end

f2 { puts "f2" }


# 使用proc與lambda
def f3(p)
  p.call
end

f3(proc{ puts "f3"})

f3(lambda{puts "f3"})

ref