Ruby之代碼塊的迷思

[b]塊的定義、調用與運行[/b]
在Ruby中,定義一個代碼塊的方式有2種 ,一是使用do … end, 另外一種是用大括號“{}”把代碼內容括起來。代碼塊定義時也是可以接受參數的。但是,只有在調用一個方法的時候纔可以定義一個塊。

塊定義好之後,會直接傳遞給調用的方法,在該方法中,使用“yield”關鍵字即可回調這個塊。
def block_method(a, b)
a + yield(a, b)
end
puts block_method(1, 2) { |a, b| a*2+b } # => 5
result = block_method(1, 2) do |a, b|
a+b*2
end
puts result # => 6

如果一個方法定義的時候使用了yield關鍵字,但是調用的時候卻沒有傳遞代碼塊,方法會拋出“no block given (yield) (LocalJumpError)”異常。可以使用Kernal#block_given?方法檢測當前的方法調用是否包含代碼塊。
def check_block
return yield if block_given?
'no block'
end
puts check_block{ 'This is a block'} # => This is block
puts check_block # => no block

代碼在運行的時候,除了需要代碼外,還需要運行環境,即一組各種變量的綁定。代碼塊就是由代碼和一組綁定組成的,代碼塊可以獲得自己定義的局部變量的綁定,和上下文可見的實例變量,在代碼塊執行的時候,只會根據自己定義時可見的綁定來執行。業界把塊這樣的特性稱之爲閉包(Closure)。
def closure_method
x = "Goodbye"
yield("@xianlinbox")
end
x = "Hello"
puts closure_method { |y| "#{x} World, #{y}!" } # => Hello World, @xianlinbox!

[b]作用域[/b]
前面提到代碼運行時,需要一組綁定, 這組綁定在代碼的運行過程中,還會發生變化,這種變化發生的根本原因就是作用域發生改變,每個變量綁定都有自己的作用域,一但代碼切換作用域,舊的綁定就會被一批新的綁定取代。在Ruby的世界中,作用域相對簡單,作用域之間是截然分開的,一旦進入另外一個作用域,原先的綁定就會替換爲一組新的綁定,可以通過Kernal#local_variables方法查看當前作用域下的綁定。那麼,程序什麼時候會從一個作用域跳轉到另一個作用域呢?

Ruby程序只會在3個地方關閉前一個作用域,同時打開一個新的作用域:
[list]
[*]類定義, class … end
[*]模塊定義, module … end
[*]方法定義, def … end
[/list]
這三個地方通常稱之爲作用域們(Scope Gate)。
v1 = 1
class MyClass
v2 = 2
puts local_variables.to_s + "call 1" # => [:v2]call 1
def my_method
v3 = 3
puts local_variables.to_s + " call 2"
end
puts local_variables.to_s + "call 3" # => [:v2]call 3
end
obj = MyClass.new
obj.my_method # =>[:v3] call 2
puts local_variables.to_s + "call 4" # =>[:v1, :obj]call 4

因爲作用域之間的隔離,讓人忍不住會想,怎樣才能讓一個綁定穿越多個作用域?在Ruby中有一個叫做扁平化作用域(Flat Scope)的概念,通俗點說就是變身拆遷隊長,拆掉所有的作用域門。
[list]
[*]用Class.new()方法代替class關鍵字來定義類
[*]使用Module。new()方法代替module關鍵字定義模塊
[*]使用Module#define_method代替def關鍵字定義方法。
[/list]
v1 = 1
MyClass = Class.new do
v2 = 2
puts local_variables.to_s + "call 1" # => [:v2, :v1]call 1
define_method :my_method do
v3 = 3
puts local_variables.to_s + " call 2"
end
puts local_variables.to_s + "call 3" # => [:v2, :v1]call 3
end
MyClass.new.my_method # => [:v3, :v2, :v1] call 2
puts local_variables.to_s + "call 4" # => [:v1]call 4

[b]instance_eval()和instance_exec()[/b]
在Ruby中,提供了一個非常酷的特性,可以通過使用Objec#instance_eval(), Objec#instance_exec()方法插入一個代碼塊,做一個的對象上下文探針(Context Proble),深入到對象中的代碼片段,對其進行操作。有了這個特性以後,就可以很輕鬆的測試對象的行爲,查看對象的當前狀態。
class MyClass
def initialize
@v = 1;
end
end
obj = MyClass.new
obj.instance_eval do
puts self # => #<MyClass:0x007fbb2d0299b0>
puts @v # => 1
end
obj.instance_exec(5) { |x| puts x * @v } # => 5

[b]可調用對象[/b]
從底層來看,對代碼塊的使用分爲2步,
* 打包代碼備用
* 調用yiel執行代碼
這種先打包,後執行的策略稱之爲延遲執行(Deferred Evaluation)。

代碼塊在Ruby中並不是一個對象,但是,如果你想把一個代碼塊保存爲一個對象以供調用應該怎麼辦呢? Ruby提供了Proc類,其簡單來說就是一個轉換成對象的塊。在Ruby中,把一個塊轉換爲Proc對象的方法有以上4種:
[list]
[*]* proc{…}
[*]* Proc.new { …}
[*]* lambda{…}
[*]* &操作符
[/list]
proc1 = proc { |x| x+1 }
puts proc1.class # => Proc
proc2 = Proc.new { |x| x+1 }
puts proc2.class # => Proc
proc3 = lambda { |x| x+1 }
puts proc3.class # => Proc

&操作符只有在方法調用時纔有效,在方法定義時,可以給方法添加一個特殊的參數,該參數必須爲參數列表中的最後一個,且以&符號開頭,其含義就是,這是一個Proc對象,我想把它當做一個塊來用,如果調用該函數時,沒有傳遞代碼塊,那麼該參數值將爲nil。有了這個符號之後,就可以很容易的把一個代碼塊轉換爲Proc對象,並在多個方法之間傳遞。
def my_method(&block)
test(&block)
block
end
def test
puts yield(3) if block_given? # => 4
end
p2 = my_method{ |x| x+1 }
puts p2.class # => Proc

Ruby中,所有的可調用對象,最後都是一個Proc對象,爲什麼還需要使用lambda這個方法來創建一個可調用對象呢? 使用lambda和使用proc創建的Proc對象有一些些微的差別,主要體現在2個方面:
[list]
[*]return關鍵字的行爲,lambda中,return僅表示從lambda中返回, 而proc中,則是從定義proc的作用域中返回。
[*]參數校驗規則:lambda中,參數個數不對,會拋ArgumentError錯誤,而proc中,則會嘗試調整參數爲自己期望的形式,參數多,則忽略多餘的,參數少則,自動補nil。
[/list]
def proc_method
p = proc { return 10 };
result = p.call
return result*2
end

def lambda_method
p = lambda { return 10 };
result = p.call
return result*2
end

puts proc_method # => 10
puts lambda_method # => 20

p1 = proc { |a, b| [a,b] }
p2 = lambda { |a, b| [a,b] }

puts p1.call(1, 2, 3).to_s # => [1, 2]
puts p1.call(1).to_s # => [1, nil]
puts p2.call(1, 2, 3).to_s # => ArgumentError
puts p2.call(1).to_s # => ArgumentError
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章