3

我试图更好地理解 Ruby 闭包,但遇到了这个我不太理解的示例代码:

def make_counter
  n = 0
  return Proc.new { n = n + 1 }
end

c = make_counter
puts c.call # => this outputs 1
puts c.call # => this outputs 2

当我打电话时,有人可以帮助我理解上述代码中实际发生的情况c = make_counter吗?在我看来,这就是我认为正在发生的事情:

Ruby 调用该make_counter方法并返回一个 Proc 对象,其中与 Proc 关联的代码块将是{ n = 1 }. 执行第一个时c.call,Proc 对象将执行与其关联的块,并返回n = 1. 但是,当执行第二个时c.call,Proc 对象是否仍然执行与其关联的块,它仍然是{ n = 1 }?我不明白为什么输出会变为 2。

也许我根本不理解这一点,如果您能提供一些关于 Ruby 中实际发生的事情的说明将会很有帮助。

4

3 回答 3

8

make_counter调用该块时不会评估该块。当您通过调用 Proc 时,该块被评估并运行c.call。所以每次你运行c.call时,表达式n = n + 1都会被计算并运行。Proc 的绑定将导致n变量保留在范围内,因为它(局部n变量)首先在 Proc 闭包之外声明。因此,n将在每次迭代中不断增加。

为了进一步澄清这一点:

  • 定义 Proc(或 lambda)的块在初始化时不会被评估 - 里面的代码就像你看到的那样被冻结。
  • 好的,代码实际上是“评估”的,但不是为了更改冻结代码。相反,它会检查当前在 Proc 代码块上下文中使用的范围内的任何变量。因为n它是一个局部变量(正如之前定义的那样),并且它在 Proc 中使用,所以它在绑定中被捕获并随之而来。
  • 当在callProc 上调用该方法时,它将在已捕获的绑定的上下文中执行“冻结”代码。因此n,最初分配为 0 的 将递增到 1。再次调用时,它n会再次递增到 2。依此类推...
于 2013-02-15T04:35:17.700 回答
1

我总是想了解发生了什么,重新审视基础知识总是很重要。对于阅读这篇文章的新手来说,没有人回答过ProcRuby 中的 a 是什么的问题,这将是至关重要的,并且有助于回答这个问题。

在高层次上,procs 是可以存储在 variables 中的方法

Procs也可以将代码块作为其参数,在这种情况下它采用n = n + 1. 在其他编程语言中,块称为闭包。块允许您将语句组合在一起并封装行为。

在 Ruby 中创建块有两种方法。您提供的示例使用大括号语法。

那么,Procs如果您可以使用方法来执行相同的功能,为什么还要使用呢?

答案是 Procs 比方法给你更多的灵活性。Procs您可以将一整套流程存储在一个变量中,然后在程序中的任何其他位置调用该变量。

在这种情况下,Proc被写在一个方法中,然后该方法被存储在一个被调用的变量中c,然后puts每次递增的值都会被调用n

Procs 类似,Lambdas还允许您将函数存储在变量中并从程序的其他部分调用方法。

于 2018-10-28T02:59:17.630 回答
0

这里:

return Proc.new { n = n + 1 }

实际上,返回一个 proc 对象,该对象有一个与之关联的块。Ruby 创建了一个带有块的绑定!所以执行上下文被存储起来供以后使用,因此我们可以增加 n。让我进一步解释 Ruby 闭包,以便您有更广泛的想法。

首先,我们需要澄清技术术语“绑定”。在 Ruby 中,绑定对象将执行上下文封装在程序的某个特定范围内,并保留此上下文以供将来在程序中使用。此执行上下文包括传递给方法的参数和方法中定义的任何局部变量、任何关联的块、返回堆栈和 self 的值。举个例子:

class SomeClass 
  def initialize
    @ivar = 'instance variable'
  end

  def m(param)
    lvar = 'local variable'
    binding
  end
end


b = SomeClass.new.m(100) { 'block executed' }
 => #<Binding:0x007fb354b7aca0>

eval "puts param", b
=> 100
eval "puts lvar", b
=> local variable
eval "puts yield", b
=> block executed
eval "puts self", b
=> #<SomeClass:0x007fb354ad82e8>
eval "puts @ivar", b
instance variable

最后一条语句可能看起来有点棘手,但事实并非如此。记住绑定保存执行上下文以供以后使用。所以当我们调用yield时,它调用yield就好像它仍然在那个执行上下文中一样,因此它调用了块。

有趣的是,您甚至可以在闭包中重新分配局部变量的值:

eval "lvar = 'changed in eval'", b
eval "puts lvar", b
=> changed in eval

现在这一切都很可爱,但没那么有用。绑定非常有用,因为它与块有关。Ruby 将绑定对象与块相关联。因此,当您创建 proc 或 lambda 时,生成的 Proc 对象不仅包含可执行块,还包含块使用的所有变量的绑定。

您已经知道块可以使用在块外定义的局部变量和方法参数。例如,在以下代码中,与 collect 迭代器关联的块使用方法参数 n:

# multiply each element of the data array by n
def multiply(data, n)
 data.collect {|x| x*n }
end
puts multiply([1,2,3], 2) # Prints 2,4,6

更有趣的是,如果将块转换为 proc 或 lambda,即使在它作为参数的方法返回之后,它也可以访问 n。那是因为有一个绑定与 lambda 或 proc 对象的块相关联!以下代码演示:

# Return a lambda that retains or "closes over" the argument n
def multiplier(n)
 lambda {|data| data.collect{|x| x*n } }
end
doubler = multiplier(2) # Get a lambda that knows how to double
puts doubler.call([1,2,3]) # Prints 2,4,6

multiplier 方法返回一个 lambda。因为这个 lambda 是在定义它的范围之外使用的,所以我们称它为闭包;它封装或“关闭”(或仅保留)方法参数 n 的绑定。

重要的是要理解闭包不仅仅保留它所引用的变量的值——它保留了实际的变量并延长了它们的生命周期。另一种说法是,在创建 lambda 或 proc 时,lambda 或 proc 中使用的变量不是静态绑定的。相反,绑定是动态的,并且在执行 lambda 或 proc 时会查找变量的值。

于 2018-10-26T03:04:38.350 回答