注:此文翻译整理自https://cirw.in/blog/constant-lookup ,推荐关注,pry 主要贡献人
翻译的难免有错,请见谅。
使用常量很简单,理解如何查找常量的很麻烦。
虽然一句话就可以说清楚常量查找,依次在 Module.nesting, Module.nesting.first.ancestors 中查找,如果 Module.nesting.first 是 nil,则在 Object.ancestors 中查找。,但是为了更好的理解,我们还是举几个例子吧。
Ruby 首先会根据词法作用域 (lexical scope) 周围的 module 或者 classes 的关联的常量来查找:
module A
module B; end
module C
module D
B == A::B
end
end
end
上面的例子里在 D 中的 B 首先会查找:A::C::D::B (不存在), 然后 A::C::B (不存在), 最后 A::B (找到了)。
跟 ruby 里的其他东西一样,查找常量的 namespace chains 也是在运行时通过 Module.nesting 可以实现自省的(运行时知道自己的 Module.nesting):
module A
module C
module D
Module.nesting == [A::C::D, A::C, A]
end
end
end
如果你尝试过用简写的声明方式来重打开一个 module,你可能已经注意到不使用完整的 namespace 是无法访问其他常量的。这是因为外面的 namespace 并没有添加到 Module.nesting 当中:
module A
module B; end
end
# 错误
module A::C
B
end
# 正确
module A
module D
B
end
end
# NameError: uninitialized constant A::C::B
上面错误的例子中,如果要使用 B,那么 Module.nesting 里必须包含 A, 但是上面的 nesting 里只有 [A::C]
如果在 Module.nesting 中找不到,Ruby 就会在当前打开 class 或者 module 的祖先中查找。
class A
module B; end
end
class C < A
B == A::B
end
当前打开是指最里面的 class 或者 module,例如上面查找 B 的 C。一个常见的误解就是常量查找会使用 self.class,例如上面也许觉得 B 应该查找 C::B,但实际上并不会这样查找,参考下面的例子。
class A
def get_c; C; end
end
class B < A
module C; end
end
B.new.get_c #self.class是B,但是并不会查找B::C 当前打开类是A,
# NameError: uninitialized constant A::C
在顶级作用域的时候,Module.nesting 是 [],所以常量直接在当前打开的 class 和他的祖先里查找,虽然你看不到,但是顶级作用域里的当前打开类是 Object。
class Object
module C; end
end
C == Object::C
虽然还没有说,但是你也应该注意到,新定义的常量也会在当前打开类里进行定义,所以顶级作用域里定义的常量也会在 Object 里进行定义:
module C; end
Object::C == C
这个就解释了为什么在顶级作用域里定义的常量在哪里都可以使用,因为在 ruby 里大部分类都是 Object 的子类,所以 Object 总是在其他类的祖先链里,所以在顶级作用域里定义的常量在其他地方都可以使用。
所以如果你使用过 BasicObject(不是 Objet 的子类),你会注意到你在 BasicObject 内部无法访问顶级作用域的常量。
class Foo < BasicObject
Kernel
end
# NameError: uninitialized constant Foo::Kernel
如果你想在这种情况下使用,可以使用::Kernel 来访问 Object::Kernel。
Ruby 假设你会把 module 混入到继承自 Object 的 class 之中,所以如果当前打开的是一个 module,也会把 Object 添加到祖先链之中。所以可以在 module 中使用顶级作用域的常量。
module A; end
module B;
A == Object::A
end
正如上面提到的,常量会查找当前打开的类或者 module,当前打开取决于周围的 class 或者 module 关键字,所以,如果你使用 class_eval 或者 module_eval 来执行 block,这个并不会修改当前打开类。还是会在 block 定义的作用域内查找。
class A
module B; end
end
class C
module B; end
A.class_eval{ B } == C::B
end
不过略复杂的事实是,如果你使用字符串来 eval,在 class_eval 的情况下,Module.nesting 中会包含自己的 class,在 instance_eval 的情况下,Module.nesting 中则会包含这个 object 的 singleton class。
class A
module B; end
end
class C
module B; end
A.class_eval("B") == A::B
end
最后我想指出,你在一个 class 的 singleton 里是无法访问在 class 内定义的常量。
class A
module B; end
end
class << A
B
end
# NameError: uninitialized constant Class::B
这是因为 singleton class 的祖先链中并不包含自身的 class,直接从 Class 开始。
class A
module B; end
end
class << A; ancestors; end
[Class, Module, Object, Kernel, BasicObject]
相似的情况下,还需要指出已经在 Module.nesting 里的 class 的父类并不会出现在 Module.nesting 之中,如下面的 A。
class A
module B; end
end
class C < A
class D
B
end
end
# NameError: uninitialized constant C::D::B
常量查找并不难,只需要记住查找词法包围的 class 和 module,你可以使用 Module.nesting 里的第一个值来当作当前打开类或者模块,如果这个值为空,则当前打开类是 Object。
下面的代码模拟了 ruby 内部的常量查找方式,你会注意到我使用字符串作为 binding.eval 的变量所以 Module.nesting 是 binding 的 object 而不是 block。
class Binding
def const(name)
eval <<-CODE, __FILE__, __LINE__ + 1
modules = Module.nesting + (Module.nesting.first || Object).ancestors
modules += Object.ancestors if Module.nesting.first.class == Module
found = nil
modules.detect do |mod|
found = mod.const_get(#{name.inspect}, false) rescue nil
end
found or const_missing(#{name.inspect})
CODE
end
end
(翻译完)
实际上常量查找是很好玩的一个过程,可以参考一下 Rails 实现 const_missing 和 const_get 的方法。
可以解决诸多疑惑,例如:
class Person
end
String.const_get(:Person) # Person
String::Person # Person
String::Person::String # String