深入探索

从方法中返回块

早些时候,我解释过 Ruby 中的块可能视为“闭包”(closures)。闭包可以说是封闭声明它的“环境”(environment)。或者,换句话说,它将局部变量的值从其原始作用域带入不同的作用域。我之前给出的示例显示了名为 ablock 的块如何捕获局部变量 x 的值…

block_closure.rb
  1. x = "hello world"
  2. ablock = Proc.new { puts( x ) }

…然后它就能够将该变量“携带”到不同的作用域内。例如,这里将块传递给 aMethod。当在该方法内部调用 ablock 时,它运行代码 puts(x)。这里显示,”hello world” 而不是 “goodbye”…

  1. def aMethod( aBlockArg )
  2. x = "goodbye"
  3. aBlockArg.call #<= displays "hello world"
  4. end

在这个特定的例子中,这种行为似乎对好奇心没有太大吸引力。实际上,可以更具创造性地使用块/闭包。

例如,你可以在方法内创建一个块并将该块返回给调用代码,而不是创建一个块并将其发送到方法。如果创建块的方法碰巧接收参数,则可以使用该参数初始化块。

这为我们提供了一种从同一“块模板”(block template)创建多个块的简单方法,每个块的实例都使用不同的数据进行初始化。例如,在这里我创建了两个块,分配给变量 salesTaxvat,每个块根据不同的值(0.10)和(0.175)计算结果:

block_closure2.rb
  1. def calcTax( taxRate )
  2. return lambda{
  3. |subtotal|
  4. subtotal * taxRate
  5. }
  6. end
  7. salesTax = calcTax( 0.10 )
  8. vat = calcTax( 0.175 )
  9. print( "Tax due on book = ")
  10. print( salesTax.call( 10 ) ) #<= prints: 1.0
  11. print( "\nVat due on DVD = ")
  12. print( vat.call( 10 ) ) #<= prints: 1.75

块与实例变量

块的一个不太明显的特性是它们使用变量的方式。如果一个块可能真的被视为匿名函数或方法,那么从逻辑上讲,它应该能够:1)包含它自己的局部变量;2)能够访问该块所属的对象的实例变量。

我们先来看实例变量(instance variables)。加载 closures1.rb 程序。这提供了块等同于闭包的另一个例子 - 通过捕获创建它的作用域中的局部变量的值。这里我使用 lambda 方法创建了块:

closures1.rb
  1. aClos = lambda{
  2. @hello << " yikes!"
  3. }

这个块将一个字符串 “yikes!” 附加到一个实例变量 @hello。请注意,在这个过程中,之前没有为 @hello 分配任何值。

但是,我创建了一个单独的方法 aFunc,它为一个名为 @hello 的变量赋值:

  1. def aFunc( aClosure )
  2. @hello = "hello world"
  3. aClosure.call
  4. end

当我将块传递给该方法(aClosure 参数)时,aFunc 方法将引入 @hello 。我现在可以使用 call 方法执行块内代码。当然 @hello 变量包含字符串 “hello world”。通过调用块也可以使用方法之外相同的变量。实际上,现在,通过反复调用块,我最终会反复追加字符串 “yikes!” 到 @hello

  1. aFunc(aClos) #<= @hello = “hello world yikes!”
  2. aClos.call #<= @hello = “hello world yikes! yikes!”
  3. aClos.call #<= @hello = “hello world yikes! yikes! yikes!”
  4. aClos.call # ...and so on
  5. aClos.call

如果你认为这并不是太令人惊讶。毕竟,@hello 是一个实例变量,因此它存在于一个对象的作用域内。当我们运行 Ruby 程序时,会自动创建一个名为 main 的对象。所以我们应该期望在该对象(我们的程序)中创建的任何实例变量可用于其中的所有内容。

现在出现的问题是:如果要将块发送到某个其它对象的方法会发生什么?如果该对象有自己的实例变量 @hello,那么该块会使用哪个变量 - 来自创建块的作用域内的 @hello,还是来自调用该块的对象作用域内的 @hello?让我们尝试一下。我们将使用与以前相同的块,除了这次它将显示有关块所属对象和 @hello 值的一些信息:

  1. aClos = lambda{
  2. @hello << " yikes!"
  3. puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
  4. }

现在从新类(X)创建一个新对象,并为它提供一个接收我们的块 b 的方法,并调用该块:

  1. class X
  2. def y( b )
  3. @hello = "I say, I say, I say!!!"
  4. puts( " [In X.y]" )
  5. puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
  6. puts( " [In X.y] when block is called..." )
  7. b.call
  8. end
  9. end
  10. x = X.new

要测试它,只需将块 aClos 传递给 xy 方法:

  1. x.y( aClos )

这就是显示的内容:

  1. [In X.y]
  2. in #<X:0x32a6e64> object of class X, @hello = I say, I say, I say!!!
  3. [In X.y] when block is called...
  4. in main object of class Object, @hello = hello world yikes! yikes! yikes! yikes! yikes! yikes!

因此,很明显,块在创建它的对象(main)的作用域内执行,并保留该对象的实例变量,即使在调用块的对象的作用域内有一个具有相同名称和不同值的实例变量。

块与局部变量

现在让我们看看块/闭包(block/closure)如何处理局部变量(local variables)。加载 closures2.rb 程序。首先,我声明一个变量 x,它对程序本身的上下文来说是局部的:

closures2.rb
  1. x = 3000

第一个块/闭包称为 c1。每次我调用这个块时,它会获取块本身外部定义的 x 值(3000)并返回 x+100

  1. c1 = lambda{
  2. return x + 100
  3. }

这个块没有块参数(也就是说,竖条之间没有’块局部’(block local)变量)所以当用变量 someval 调用它时,该变量被丢弃,未使用。换句话说,c1.call(someval)c1.call() 具有相同的效果。所以当你调用块 c1 时,它返回 x+100(即 3100),然后将该值赋给 someval

  1. someval=1000
  2. someval=c1.call(someval); puts(someval) #<= someval is now 3100
  3. someval=c1.call(someval); puts(someval) #<= someval is now 3100
注意:如上所示,你可以将调用放在块中并将其传递给 Integer 的 times 方法,而不是重复调用 c1,如下所示: 2.times{ someval=c1.call(someval); puts(someval) }但是,因为它可能很难在只有一个块(例如这里的 c1 块)的情况下工作,以至于我故意避免使用比这个程序中更多本应该必要的块!

第二个块名为 c2。这声明了一个’块参数’(block parameter),z。这也返回一个值:

  1. c2 = lambda{
  2. |z|
  3. return z + 100
  4. }

但是,这次返回值可以重复使用,因为块参数就像一个方法的传入参数 - 所以当 someval 的值在被赋值为 c2 的返回值之后被更改时,这个更改的值随后作为参数传入:

  1. someval=1000
  2. someval=c2.call(someval); puts(someval) #<= someval is now 1100
  3. someval=c2.call(someval); puts(someval) #<= someval is now 1200

乍一看,第三个块 c3 与第二个块 c2 几乎相同。实际上,唯一的区别是它的块参数被称为 x 而不是 z

  1. c3 = lambda{
  2. |x|
  3. return x + 100
  4. }

块参数的名称对返回值没有影响。和以前一样,someval 首先被赋值 1100(即,它的原始值 1000,加上块中添加的 100)然后,当第二次调用块时,someval 被赋值为 1200(其先前的值 1100,加上在块内分配的 100)。

但现在看一下局部变量 x 的值会发生什么。在该单元的顶部分配了 3000。只需给块参数指定相同的名称 x,我们就改变了局部变量 x 的值。它现在具有值 1100,即块参数 x 在调用 c3 块时最后具有的值:

  1. x = 3000
  2. c3 = lambda{
  3. |x|
  4. return x + 100
  5. }
  6. someval=1000
  7. someval=c3.call(someval); puts(someval)
  8. someval=c3.call(someval); puts(someval)
  9. puts( x ) #<= x is now 1100

顺便提一下,即使块局部变量和块参数可以影响块外部的类似命名的局部变量,块变量本身也不会在块之外存在。你可以使用 defined? 关键字对此进行验证,以尝试显示变量的类型(如果确实已定义):

  1. print("x=[#{defined?(x)}],z=[#{defined?(z)}]")

Ruby 的创造者 Matz,他将块内局部变量的作用域描述为“抱歉的”(regrettable)。特别是,他认为在一个块中使局部变量对包含该块的方法不可见是错误的。有关此示例,请参阅 local_var_scope.rb

local_var_scope.rb
  1. def foo
  2. a = 100
  3. [1,2,3].each do |b|
  4. c = b
  5. a = b
  6. print("a=#{a}, b=#{b}, c=#{c}\n")
  7. end
  8. print("Outside block: a=#{a}\n") # Can't print #{b} and #{c} here!!!
  9. end

这里,块参数 b 和块局部变量 c 只有在块本身内部时才可见。该块可以访问这些变量并作用于变量 afoo 方法的局部)。但是,在块之外,bc 是不可访问的,只有a 是可见的。

只是为了增加迷惑性,块局部变量 c 和块参数 b 在上面的示例中都不能在块外部访问,但是当你用一个for 块迭代时可以访问它们,如下例所示:

  1. def foo2
  2. a = 100
  3. for b in [1,2,3] do
  4. c = b
  5. a = b
  6. print("a=#{a}, b=#{b}, c=#{c}\n")
  7. end
  8. print("Outside block: a=#{a}, b=#{b}, c=#{b}\n")
  9. end

在 Ruby 的未来版本中,在块内赋值的局部变量(与 c 一样)也将是块外部方法(例如 foo)的局部变量。形式上块参数(如 b)将是块的局部变量。