2

我在 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"]
...

谁能解释一下这里发生了什么?

4

2 回答 2

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 变量分配一个新创建的数组,并且新数组完全独立于 result 和 result[:a],但所有 lambda 都看到相同的数组。那是因为所有的 lambdas 都关闭了同一个 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 将引用相同的结果变量——分配给结果变量的最后一个哈希是所有 lambda 看到的哈希。

所以规则是:循环不会每次通过循环都创建新变量,而块会这样做。

请注意,最好在循环外声明所有循环变量,以免我们忘记循环内的变量不是每次循环都重新创建的。

于 2013-08-17T17:36:23.527 回答
2

通过将代码放在 中lambda,您将推迟评估,result直到它被引用,此时值已更改。当您刚刚引用时,闭包工作正常,parent_nodes因为在创建 lambda 时parent_nodes已经设置了(即result已经访问过)的值,并且parent_nodes没有重用定义的块。

请注意,如果您每次通过循环创建一个单独的块并result在该块中定义,则闭包也将起作用。看到Ruby for 循环一个陷阱?进行相关讨论。

于 2013-08-17T19:04:15.850 回答