Scope Wrap-Up

每个 Ruby 作用域都包含一组绑定,并且不同的作用域之间被 作用域门 分隔开来: classmoduledef。如果要让一两个绑定穿越作用域门,那么可以用方法调用来替代作用域门:用一个闭包获取当前的绑定,并把这个闭包传递给该方法。你可以使用 Class.new() 方法代替 class,使用 Module.new() 代替 module,以及使用 Module#define_method() 代替 def,这就形成了一个 扁平作用域,它是闭包中的一个基本概念。如果在一个扁平作用域中定义了多个方法,则这些方法可以用一个作用域门进行保护,并共享绑定,这种技术称为 共享作用域

instance_eval()

Object#instance_eval() 方法可以在一个 对象的上下文 中执行一个块:

  class MyClass
    def initialize
      @v = 1
    end
  end

  obj = MyClass.new
  obj.instance_eval do
    self                #=> #<MyClass:0x3340dc @v=1>
    @v                  #=> 1
  end

在运行时,该块的接收者会成为 self,因此它可以访问接收者的私有方法和实例变量,例如 @v。instance_eval() 方法甚至可以在不碰其他绑定的情况下修改 self 对象:

  v = 2
  obj.instance_eval { @v = v}
  obj.instance_eval { @v }      #=> 2
  • 可以把传递给 instance_eval() 方法的块称为一个 上下文探针,因为它就像一个深入到对象中的代码片段,对其进行操作。

instance_exec()

instance_exec() 方法和instance_eval() 的功能相似,但它允许对块传入参数:

  class C
    def initialize
      @x, @y = 1, 2
    end
  end

  C.new.instance_exec(3) {|arg| (@x + @y) * arg }  #=> 9

Clean Rooms

有时你会创建一个对象,仅仅是为了在其中执行块,这样的对象称为 洁净室

  class CleanRoom
    def complex_calculation
      #...
    end

    def do_something
      #...
    end
  end

  clean_room = CleanRoom.new
  clean_room.instance_eval do
    if complex_calculation > 10
      do_something
    end
  end

洁净室仅仅是一个用来执行块的环境,它通常还会暴露若干有用的方法供块调用。

Callable Objects

从底层来看,使用块需要分为两步。第一步,将代码打包备用;第二步,调用块(通过 yield 语句)来执行代码。这种“先打包代码,以后调用”的机制并不是块的专利。在 Ruby 中,至少还有以下三种方法可以用来打包代码:

  • 使用 proc。proc 基本上就是一个由块转换成的对象。
  • 使用 lambda。它是 proc 的近亲。
  • 使用方法。

Proc Objects

尽管 Ruby 中绝大多数东西都是对象,但是块不是。一个 Proc 就是一个转换成对象的块,可以通过把块传给 Proc.new 方法来创建一个 Proc,以后就可以用 Proc#call() 方法来执行这个由块转换而来的对象,这种技术称为 延迟执行

  inc = Proc.new {|x| x + 1}
  # ...
  inc.call(2)   #=> 3

Ruby 还提供了两个内核方法用于把块转换为 Proc:lambda() 和 proc()。一会就能看到 lambda() 、proc() 和 Proc.new() 这三种方式之间有一些细微的差别,但在大多数情况下,你可以随便挑一个最喜欢的方式来使用:

  dec = lambda {|x| x - 1}
  dec.class                 #=> Proc
  dec.call(2)               #=> 1