Ruby中奇怪的闭包行为

时间:2013-08-17 17:09:44

标签: ruby closures

我正在弄乱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 个答案:

答案 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?