我正在弄乱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"]
...
有人能解释到底发生了什么吗?
答案 0 :(得分: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
在循环内的lambda行中:
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
... lambda关闭整个结果变量 - 而不仅仅是结果[:a]。但是,每次通过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]
每次循环时都会为val变量分配一个新创建的数组,而新数组完全独立于结果和结果[: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线产生不同结果的原因。每次执行块时,第一个lambda行将关闭一个新的val变量。但是每次执行块时,第二个lambda行都会关闭相同的结果变量,因此所有lambda将引用相同的结果变量 - 分配给结果变量的最后一个哈希是所有lambdas看到的哈希值。
所以规则是:循环不会每次循环和块都创建新的变量。
请注意,最好在循环外声明所有循环变量,以免我们忘记每次循环时都不会重新创建循环内的变量。
答案 1 :(得分:2)
通过将代码置于lambda
内,您将推迟result
的评估,直到它被引用,此时值已更改。刚引用parent_nodes
时,闭包工作正常,因为{lambner创建时parent_nodes
的值已经设置(即已访问result
){{1}已定义未被重用。
请注意,如果每次循环创建一个单独的块并在该块中定义parent_nodes
,则闭包也将起作用。有关相关讨论,请参阅Ruby for loop a trap?。