Scope Gate

追踪作用域是一项枯燥的任务,所以,我们需要了解 作用域门,来更快地找到作用域。

准确地说,程序会在三个地方关闭前一个作用域,同时打开一个新的作用域:

  • 类定义
  • 模块定义
  • 方法

只要程序进入类或模块及方法的定义,就会发生作用域切换。这三个边界分别用 class、module、和 def 关键字作为标志,每一个关键字都充当了一个 作用域门

全局变量和顶级实例变量

全局变量可以在任何作用域中访问:

  def a_scope
    $var = "some value"
  end

  def another_scope
    $var
  end

  a_scope
  another_scope  #=> "some value"

全局变量的问题在于系统的任何部分都可以修改它们,因此,你会发现几乎没法追踪谁把它们改成了什么。正因为如此,基本的原则是:如非必要,尽可能少使用全局变量。有时可以用 顶级实例变量 来代替全局变量,它们是顶级对象 main 的实例变量:

  @var = "The top-level @var"

  def my_method
    @var
  end

  my_method   #=> "the top-level @var"

如上所示,只要 main 对象扮演 self 的角色,就可以访问一个顶级实例变量,但当其他对象成为 self 时,顶级实例变量就退出作用域了。由于不像全局变量那么有全局性,一般认为顶级实例变量比全局变量更安全。

  • class/module 与 def 之间还有一点微妙的差别:在类和模块定义中的代码会被立即执行,但方法定义中的代码只有在方法被调用时才被执行

Flattening the Scope

现在我们知道程序在哪里开始切换作用域,即在看到 class、module 或 def 关键字时开始切换作用域,但是如果希望让一个变量穿越作用域,该怎么做?

  my_var = "Success"

  class MyClass
    #你希望在这里打印 my_var ...

    def my_method
      # ...还有这里
    end
  end

作用域门是一道难以翻越的藩篱,在进入另一个作用域时,局部变量会立即失效。首先关于 class 这个作用域门,虽然不能让 my_var 穿越它,但是可以把 class 关键字替换为某个非作用域门的东西:方法。如果能用方法替换 class,就能在一个闭包中获得 my_var 的值,并把闭包传递给该方法。Class.new() 是 class 的完美替身。然后关于 def 这个作用域门,同样也需要用一个方法来替换这个关键字,可以用 Module#define_method() 方法来替代 def:

  my_var = "Success"

  MyClass = Class.new do
    puts "#{my_var} in the class definition!"

    define_method :my_method do
      puts "#{my_var} in the method!"
    end
  end

  MyClass.new.my_method

  #=> Success in the class definition!
  #=> Success in the method!

如果使用方法来替代作用域门,那么就可以让一个作用域看到另一个作用域中的变量,也就是两个作用域被挤压在一起,可以共享各自的变量,这被称为 扁平作用域。假定你想在一组方法之间共享一个变量,但是又不希望其他方法也能访问这个变量,就可以把这些方法定义在那个变量所在的扁平作用域中:

  def define_methods
    shared = 0

    Kernel.send :define_method, :counter do
      shared
    end

    Kernel.send :define_method, :inc do |x|
      shared += x
    end

    define_methods

    counter       #=> 0
    inc(4)
    counter       #=> 4

这里定义了两个 内核方法,Kernel#counter() 和 Kernel#inc() 方法都可以看到 shared 变量,而其他的方法则看不到它,因为这个变量被作用域门保护着—-这就是使用 define_methods() 的原因,这种用来共享变量的技巧称为 共享作用域