Tag Archives: Namespace

隐式上下文 `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 都会保持未定义状态。

防不胜防的命名冲突

这只是一个小经验——在使用 Apache Ant 构建 J2EE 工程的时候可能时不时会有这样的情况:编译时发生了错误 “undefined method `YYY’ for type `XXX’”,但在已包含的 `XXX’ 库定义中一查,证实 `XXX’ 类确实有 `YYY’ 方法,那为什么编译器还会报这个错呢?

之前在尝试编译一个工程时就发生了这样的情况,编译器提示“Node 类型没有定义 GetTextContent 方法”。通过 MyEclipse 提供的动态追踪功能找到了 GetContentText 方法的接收者(Node 类型)的声明地点,原来是一个处理 XML 文档的 JAR 类库,里面有一个 Node 类型,用来表示 XML 文档中的节点,但这个类中并没有定义 GetTextContent 方法。一查之下,才发现原来这是一个过期的 JAXP 库,而 JAXP 1.3(DOM 3)之前是没有提供 GetTextContent 这个接口的。出于某种原因,从 CVS 同步过来的工程包含了这个过期的 JAR 库,而由于 Ant 包含 JAR 会自动导入所有包,使得原有的 Node 类的反而被新导入的这个 Node 覆盖了,所以有了这个错误。

这是一个典型的命名冲突问题——由于两个不同的类有着相同的名字,使得它们不能同时生存在一个环境下。本来 Java 的包机制就是用来解决这个问题的,但由于 Ant 导入了所有包,使得不同命名空间的符号都被糅合到了全局命名空间中,失去了命名空间原本的命名冲突兼容性。

因此,在不使用 Ant 这样的构建工具时,我们在命名的时候一定要注意管理类和包的分配,最大限度利用命名空间,避免命名冲突。

package FSL;

public class Node {
    public Node() {
        # ...
    }
}
package global;
// import FSL.*; * Don't do this! *

public class Node {
    public Node() {
        # ...
    }
    public static void main(String[] args) {
        FSL.Node node1 = new FSL.Node();
        Node node2 = new Node();
        # ... We're good from here.
    }
}