传递命名的 Proc 参数

到目前为止,我们已经通过匿名的(在这种情况下使用 yield 关键字执行块)或以命名参数的形式将块传递给例程(procedures),在这种情况下,它使用 call 方法执行。 还有另一种传递块的方法。当方法的参数列表中的最后一个参数前面有一个 符号时,它被认为是一个 Proc 对象。这使你可以选择使用与将块传递给迭代器时相同的语法将匿名块传递给例程。然而,例程本身可以接收块作为命名参数。运行 5blocks.rb 以查看此示例。

5blocks.rb

首先,这里提醒我们已经看到过传递块的两种方式。 该方法有三个参数,a,b,c:

  1. def abc( a, b, c )
  2. a.call
  3. b.call
  4. c.call
  5. yield
  6. end

我们用三个命名参数调用这个方法(这里恰好是块,但原则上可以是任何东西)加上一个未命名的块:

  1. abc(a, b, c ){ puts "four" }

abc 方法使用 call 方法执行命名的块参数,使用 yield 关键字执行未命名的块:

  1. a.call #<= call block a
  2. b.call #<= call block b
  3. c.call #<= call block c
  4. yield #<= yield unnamed block: { puts "four" }

下一个方法 abc2 接收单个参数,&d

  1. def abc2( &d )

此处的 & 符是重要的,因为它表示 &d 参数是一个块。但是,我们不需要将此块作为命名参数发送。相反,我们只需将其附加到方法名称后即可传递未命名的块:

  1. abc2{ puts "four" }

abc2 方法不使用 yield 关键字,而是使用参数名称(没有 & 符号)执行块:

  1. def abc2( &d )
  2. d.call
  3. end

你可以将 & 符号参数视为块参数的类型检查(type-checked)。也就是说,& 参数被正式声明,因此与匿名块(那些 ‘yielded’)不同,块不会在’未通知’(unannounced)的情况下到达方法。但与普通参数(没有 & 符号)不同,它们必须匹配块。你不能将其它类型的对象传递给 abc2

  1. abc2( 10 ) # This won‟t work!

除了指定第四个正式参数 (&d) 之外,abc3 方法与 abc 方法基本相同:

  1. def abc3( a, b, c, &d)

参数 abc 被调用,而参数 &d 可以被调用(call)或产生(yield),如你所愿:

  1. def abc3( a, b, c, &d)
  2. a.call
  3. b.call
  4. c.call
  5. d.call #<= block &d
  6. yield #<= also block &d
  7. end

这意味着调用代码必须将三个普通参数和一个块(这可能是匿名的)传递给此方法:

  1. abc3(a, b, c){ puts "five" }

当接收方法没有匹配的命名参数时,你还可以使用前缀 & 符号将命名块传递给方法,如下所示:

  1. abc3(a, b, c, &myproc )

当一个 & 符号块变量传递给一个方法时(如上面的代码所示),它可能会被生成(yielded)。这提供了传递匿名块或 Proc 对象的选项:

  1. xyz{ |a,b,c| puts(a+b+c) }
  2. xyz( &myproc )

但要小心!请注意,在上面的一个示例中,我使用了块参数(|a, b, c|),其名称与我之前分配给 Proc 对象的三个局部变量的名称相同:abc

  1. a = lambda{ puts "one" }
  2. b = lambda{ puts "two" }
  3. c = proc{ puts "three" }
  4. xyz{ |a,b,c| puts(a+b+c) }

现在,原则上块参数应仅在块本身内可见。但是,事实证明,对块参数的赋值可以初始化在块的原作用域(native scope,请参阅本章前面的“什么是闭包?”)内具有与块参数相同名称的任何局部变量的值。

尽管 xyz 方法中的变量被命名为 xyz,但事实证明,该方法中的整数赋值实际上是对块中变量 abc 进行的。

  1. { |a,b,c| puts(a+b+c) }

…传递 xyz 的值:

  1. def xyz
  2. x = 1
  3. y = 2
  4. z = 3
  5. yield( x, y, z ) # 1,2,3 assigned to block parameters a,b,c
  6. end

因此,一旦块中的代码运行,块的原作用域(native scope,也是我的程序的 main 作用域)中的变量 abc 就会被块变量的值初始化:

  1. xyz{ |a,b,c| puts(a+b+c) }
  2. puts( a, b, c ) # displays 1, 2, 3

为了更清楚这一点,请尝试 6blocks.rb 中的简单程序:

6blocks.rb
  1. a = "hello world"
  2. def foo
  3. yield 100
  4. end
  5. puts( a )
  6. foo{ |a| puts( a ) }
  7. puts( a ) #< a is now 100

这是一个容易陷入 Ruby 的陷阱之一的例子。作为一般规则,当变量共享相同的作用域(例如,在此处程序的 main 作用域内声明的块)时,最好使其名称唯一,以避免任何不可预见的副作用。

请注意,此处描述的块作用域适用于 Ruby 1.8.x 以上的版本(包含此版本),在编写本文时,它可能被认为是 Ruby 的“标准”(standard)版本。Ruby 1.9 中正在对作用域进行更改,并将其合并到 Ruby 2.0 中。有关作用域的更多信息,请参阅本章末尾的“深入探索”部分中的“块和局部变量”。