【问题标题】:Weird closure behavior in RubyRuby 中奇怪的闭包行为
【发布时间】:2013-08-17 17:09:44
【问题描述】:

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

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

【问题讨论】:

    标签: ruby closures


    【解决方案1】:

    您的代码中的问题归结为:

    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 看到的哈希。

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

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

    【讨论】:

    • 这是完美的。非常感谢您所做的所有努力:)。
    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-25
      相关资源
      最近更新 更多