2013-08-17 156 views
2

我在Ruby中關閉了閉包,並遇到了以下我無法理解的情況。Ruby中的奇怪閉包行爲

def find_child_nodes(node) 
    left_node_name = "#{node}A" 
    right_node_name = "#{node}B" 
    [left_node_name, right_node_name] 
end 

# use a stack of closures (lambdas) to try to perform a breadth-first search 
actions = [] 
actions << lambda { {:parent_nodes => ['A'], :child_nodes => find_child_nodes('A') } } 

while !actions.empty? 
    result = actions.shift.call 

    puts result[:parent_nodes].to_s 

    result[:child_nodes].each do |child_node| 
    parent_nodes = result[:parent_nodes] + [child_node] 
    actions << lambda { {:parent_nodes => parent_nodes, :child_nodes => find_child_nodes(child_node) } } 
    end 
end 

上面的代碼返回以下廣度優先搜索輸出:

["A"] 
["A", "AA"] 
["A", "AB"] 
["A", "AA", "AAA"] 
["A", "AA", "AAB"] 
["A", "AB", "ABA"] 
["A", "AB", "ABB"] 
["A", "AA", "AAA", "AAAA"] 
... 

到目前爲止,一切都很好。但現在如果我改變這兩條線

parent_nodes = result[:parent_nodes] + [child_node] 
actions << lambda { {:parent_nodes => parent_nodes, :child_nodes => find_child_nodes(child_node) } } 

到這一行

actions << lambda { {:parent_nodes => result[:parent_nodes] + [child_node], :child_nodes => find_child_nodes(child_node) } } 

我的搜索不再是廣度優先。相反,我現在得到

["A"] 
["A", "AA"] 
["A", "AA", "AB"] 
["A", "AA", "AB", "AAA"] 
["A", "AA", "AB", "AAA", "AAB"] 
... 

任何人都可以解釋到底發生了什麼?

回答

2

在你的代碼的問題歸結爲:

results = [ 
    {a: [1, 2, 3]}, 
    {a: [4, 5, 6]}, 
] 

funcs = [] 

while not results.empty? 
    result = results.shift 

    2.times do |i| 
    val = result[:a] + [i] 

    #funcs << lambda { p val } 
    funcs << lambda { p result[:a] + [i] } 
    end 
end 

funcs.each do |func| 
    func.call 
end 

--output:-- 
[4, 5, 6, 0] 
[4, 5, 6, 1] 
[4, 5, 6, 0] 
[4, 5, 6, 1] 

的封閉關閉了一個變量 - 不是一個值。隨後,變量可以改變,閉包在執行時會看到新的值。這裏是一個很簡單的例子:

val = "hello" 
func = lambda { puts val } #This will output 'hello', right? 

val = "goodbye" 
func.call 

--output:-- 
goodbye 

在這裏圈裏面的拉姆達行:

results = [ 
    {a: [1, 2, 3]}, 
    {a: [4, 5, 6]}, 
] 

funcs = [] 

while not results.empty? 
    result = results.shift 
    ... 
    ... 

    funcs << lambda { p result[:a] + [i] } #<==HERE 
    end 
end 

...拉姆達關閉了整個結果變量 - 不只是導致[:一個]。但是,每次通過while循環時結果變量都是相同的變量 - 每次通過循環時都不會創建新變量。

同樣的事情發生在該代碼中的VAL變量:

results = [ 
    {a: [1, 2, 3]}, 
    {a: [4, 5, 6]}, 
] 

funcs = [] 

while not results.empty? 
    result = results.shift 
    val = result[:a] + [1] 

    funcs << lambda { p val } 
end 

funcs.each do |func| 
    func.call 
end 

--output:-- 
[4, 5, 6, 1] 
[4, 5, 6, 1] 

瓦爾變量每次分配一個新創建的數組通過循環,而新的數組是完全獨立的結果的和導致[ :a],但所有的lambda都看到相同的數組。那是因爲所有的lambda關閉了相同的val變量;那麼val變量隨後會改變。

但是,如果你介紹一個塊:

while not results.empty? 
    result = results.shift 

    2.times do |i| 
    val = result[:a] + [i] 
    funcs << lambda { p val } 
    end 
end 

--output:-- 
[1, 2, 3, 0] 
[1, 2, 3, 1] 
[4, 5, 6, 0] 
[4, 5, 6, 1] 

...每塊執行,則重新創建VAL可變時間。結果,每個lambda關閉了一個不同的val變量。如果你認爲塊只是一個傳遞給方法的函數,在這種情況下是times()方法,那應該是有道理的。然後該方法重複調用該函數 - 並調用函數時,會創建局部變量,如val;當函數完成執行時,所有的局部變量都被銷燬。

現在回到原來的例子:

while not results.empty? 
    result = results.shift 

    2.times do |i| 
    val = result[:a] + [i] 

    #funcs << lambda { p val } 
    funcs << lambda { p result[:a] + [i] } 
    end 
end 

究其原因,兩個半波線產生不同的效果,現在應該清楚了。每次執行塊時,第一個lambda行關閉一個新的val變量。但是第二個lambda行會在每次執行塊時關閉相同的結果變量,因此所有lambda表達式都會引用相同的結果變量 - 並且分配給結果變量的最後一個hash是所有lambda表達式看到的hash。

所以規則是:循環不會每次通過循環和塊創建新變量。

請注意,最好在循環外部聲明所有循環變量,以免我們忘記循環內的變量並不是每次都通過循環重新創建。

+0

這是完美的。非常感謝你所有的努力:)。 – Reck

2

通過將代碼放置在lambda中,您將推遲評估result,直到它被引用,此時值已更改。當您剛剛參考parent_nodes時,由於已創建parent_nodes的值(即已訪問result),並且定義了parent_nodes的塊未被重新使用,所以關閉工作正常。

請注意,如果每次通過循環創建一個單獨的塊並在該塊中定義result,則閉包也將起作用。有關相關討論,請參閱Ruby for loop a trap?