了解Ruby闭包

时间:2013-02-15 04:29:02

标签: ruby closures

我正在尝试更好地理解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中实际发生的事情做一些澄清,将会有所帮助。

3 个答案:

答案 0 :(得分:8)

调用make_counter时不评估块。当您通过c.call调用Proc时,将评估并运行该块。因此,每次运行c.call时,将评估并运行表达式n = n + 1。 Proc的绑定将导致n变量保留在范围内,因为它(本地n变量)首先在Proc闭包之外声明。因此,n将在每次迭代时保持递增。

进一步澄清:

  • 定义Proc(或lambda)的块在初始化时不会被评估 - 其中的代码与您看到的完全一样。
  • 好的,代码实际上是“已评估”,但不是为了更改冻结代码。而是检查当前在范围内的任何变量,这些变量在Proc的代码块的上下文中使用。由于n是一个局部变量(因为它之前定义了该行),并且它在Proc中使用,因此它会在绑定中被捕获并出现在骑行中。
  • 当在Proc上调用call方法时,它将在已捕获的绑定的上下文中执行“冻结”代码。因此,最初被指定为0的n递增为1.再次调用时,相同的n将再次递增为2.依此类推......

答案 1 :(得分:0)

这里:

return Proc.new { n = n + 1 }

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

首先,我们需要澄清技术术语“绑定”。在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

乘数方法返回一个lambda。由于此lambda在定义范围之外使用,因此我们称其为闭包。它封装或“封闭”(或保留)方法参数n的绑定。

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

答案 2 :(得分:0)

我总是想了解发生了什么,重新了解基础知识总是很重要的。没有人回答过Ruby中的Proc是什么问题,对于新手来说,阅读这篇文章非常关键,将有助于回答这个问题。

从高层次来看,过程是可以存储在变量内部的方法

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

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

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

答案是Procs比方法给您更大的灵活性。使用Procs,您可以将整个过程集存储在一个变量中,然后在程序中的其他任何地方调用该变量。

在这种情况下,将Proc写入方法中,然后将该方法存储在名为c的变量中,然后每次将{{1 }}。

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

相关问题