隐式上下文 `cref’


不要盲目地认为当前的 `self’ 同时也决定了常量的上下文。Ruby 有三个隐式上下文:`self’、`klass’ 以及 `cref’。`self’ 和 `klass’,用一句话总结就是:`self’ 决定了调用未指定接收者的方法时的默认接收者以及如何解析当前作用域下的变量,而 `klass’ 决定了当前定义的方法属于哪个类或模块。而最后一个 `cref’,则是决定了当前的常量作用域,也就是在引用常量时决定在哪个类或模块查找,定义常量时决定它最终归属于哪个类或模块。再换句话说,就是决定了当前的常量命名空间。

先来看一个例子:

X = Class.new {}

这段代码中,Class.new 的块开启了一个新的作用域作为正在分配的 Class 对象的主体。很多人在这里就想当然地认为这个块相当于 class X … end,在块中所写的代码会和 class X … end 之间的代码有相同的效果,而这是错误的。在这个块中,只有 `self’ 和 `klass’ 这两个隐式上下文被设为了正在分配的这个 Class 对象本身,但 `cref’ 没有变。

X = Class.new do
  p self
  def foo
    p :foo
  end
end
 
x = X.new 
x.foo

这段代码输出了 Class 对象本身和 `:foo’,就是因为 `self’ 和 `klass’ 都指向了这个 Class 对象本身。然而,当你这样做的时候:

X = Class.new { Y = 1 } 
p X::Y

解释器抛出一个警告:

1.rb:5: warning: toplevel constant Y referenced by X::Y

这个警告是提醒用户,Y 是顶层常量,不需要用 X 来修饰它[1],直接引用 Y 就可以了。

很明显,这是因为当前的常量上下文没有因为程序执行进入了 Class.new 的块而改变,所以在块内部定义常量的时候,`cref’ 仍然是顶层。Object#instance_eval 和 Module#class_eval 影响的也只有 `self’ 和 `klass’,但不会改变 `cref’。那么什么才会使得 `cref’ 改变?只有语法上的 class … end 和 module … end 定义。

class X
  Y = 1
end

p X::Y         # => 1

那么如何不在不建立 class … end、module … end 结构的情况下显式指定常量归属?直接通过 `::’ 运算符修饰在某些场合下好使,但这在像 X = Class.new { … } 这样的场合下是不会成功的,因为在块执行的时候 X 还没有定义[2]。同时,这种方法是对 `X’ 这个符号做了硬性编码,但有时我们可能会需要让这个命名空间本身是不定的(变量),比如在 Class.new 的块中定义属于正在分配的这个 Class 对象的常量。这时,有一对很好用的方法就是你的朋友:Module#const_get 和 Module#const_set。

X = Class.new { const_set :Y, 1 }

p X::Y           # => 1
p X.const_get :Y # => 1

由于 const_get 和 const_set 的接收者就是它们操作常量时所使用的命名空间,动态性就实现了。

[1] 用 X 修饰常量则表示常量定义在 X 这个命名空间中。
[2] Class.new { … } 连块一起,整个返回后才会把返回值赋给 X,在此之前 X 都会保持未定义状态。

Leave a comment