Back to the Basics

  def a_method(a, b)
    a + yield(a, b)
  end

  a_method(1, 2) {|x, y| (x + y) * 3}        #=> 10

只有在调用一个方法时才可以定义一个块,块会被直接传递给这个方法,然后该方法可以用 yield 关键字回调这个块。

  def a_method
    return yield if block_given?
    'no block'
  end

  a_method                          #=> "no block"
  a_method { "here's a block!" }    #=> "here's a block!"

在一个方法中,可以向 Ruby 询问当前的方法调用是否包含块。这可以通过 Kernel#block_given?() 方法来做到。

  module Kernel
    def using(resource)
      begin
        yield
      ensure
        resource.dispose
      end
    end
  end

我们不能定义一个关键字,但是可以通过 内核方法 来伪造一个。 Kernel#using() 方法以待管理的资源作为参数,它还要接受一个块,在那里执行代码。无论块中的代码是否正常执行完成, ensure 语句都会调用 resource 的 dispose() 方法来释放它。

Closures

块不仅仅是一段浮动的代码,当代码运行时,它需要一个执行环境:局部变量、实例变量、self……这些东西是绑定在对象上的名字,简称为 绑定。块的要点在于它是完整的,它既包含代码,也包含一组绑定。

  • 当定义一个块时,它会获取当时环境中的绑定,并且当把它传给一个方法时,它会带着这些绑定一起进入该方法:
    def my_method
      x = "Goodbye"
      yield("cruel")
    end
    
    x = "Hello"
    my_method {|y| "#{x}, #{y} world "}     #=> "Hello, cruel world"
    

    虽然在方法中定义了一个变量 x,块看到的 x 也是在块定义时绑定的 x,但是方法中的 x 对这个块来说是不可见的。基于这样的特性,计算机科学家喜欢把块称为 闭包。对我们而言,这意味着一个块可以获取局部绑定,并一直带着它们。

Scope

下面的例子演示了在程序运行时作用域是怎样切换的,它会用 Kernel#local_variables() 方法来跟踪绑定的名字:

  v1 = 1

  class MyClass
    v2 = 2
    local_variables          #=> [:v2]

    def my_method
      v3 = 3
      local_variables
    end

    local_variables          #=> [:v2]
  end

  obj = MyClass.new
  obj.my_method              #=> [:v3]
  obj.my_method              #=> [:v3]  (这个 v3 跟上面那个 v3 无关)
  local_variables            #=> [:v1, :obj]

在一些语言中,比如 Java 和 C#,有“内部作用域”的概念。在内部作用域中可以看到“外部作用域”中的变量。但 Ruby 中没有这种嵌套式的作用域,它的作用域是截然分开的:一旦进入一个新的作用域,原先的绑定就会替换为一组新的绑定。这意味着在程序进入 MyClass 后, v1 便超出作用域范围,从而就不可见了。

在定义 MyClass 的作用域中,程序定义了 v2 及一个方法。因为方法中的代码还没有被执行,所以直到类定义结束前,程序不会再打开一个新的作用域。在方法定义完成后,用 class 关键字打开的作用域会永远关闭,同时程序回到顶级作用域。

当程序第一次进入 my_method() 方法时,它会打开一个新的作用域并定义一个局部变量 v3,接着程序退出这个方法,回到顶级作用域。当程序第二次调用 my_method() 方法时,它会打开另外一个新的作用域,并且定义另外一个新的 v3 变量。

无论何时,只要程序切换了作用域,一些绑定就会被全新的绑定所取代。当然,并不是对所有的绑定都如此。如果一个对象调用同一个对象中的另外一个方法,那么实例变量在调用过程中始终存在于作用域中。尽管如此,绑定在切换作用域时往往会掉出作用域。尤其是在切换作用域时,局部变量总是会掉出的。