Ruby Ruby 常量查找 (译)

taojay315 · 2016年01月11日 · 最后由 taojay315 回复于 2016年01月12日 · 3764 次阅读

不好意思没注意到,跟这篇重复了,管理员看到顺手帮我删了吧。 https://ruby-china.org/topics/26890

注:此文翻译整理自https://cirw.in/blog/constant-lookup ,推荐关注,pry 主要贡献人

翻译的难免有错,请见谅。

使用常量很简单,理解如何查找常量的很麻烦。

虽然一句话就可以说清楚常量查找,依次在 Module.nesting, Module.nesting.first.ancestors 中查找,如果 Module.nesting.first 是 nil,则在 Object.ancestors 中查找。,但是为了更好的理解,我们还是举几个例子吧。

Module.nesting

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]

Ancestors

如果在 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

Object::

在顶级作用域的时候,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

class_eval

正如上面提到的,常量会查找当前打开的类或者 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  

把代码高亮一下呗

开头这样写就可以 ```ruby

辛苦辛苦,不过有篇相似度很高的文章,https://ruby-china.org/topics/26890

#2 楼 @qinfanpeng 额 是重复了 我没注意到,现在正在找删帖功能。。

感觉不用删吧,还是有不同地方的,留着吧。貌似有人评论过的帖子就不能被删除了。

#4 楼 @qinfanpeng 额 还不可以删除回复。。。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号