prefer:http://urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/
参考这篇文章,总结的已经很好了,有改动和补充。
Ruby Constant Lookup(注意这里是Ruby的)
Constant lookup in Ruby is reasonably simple, once you know the rules, but it isn’t always totally intuitive. When you refer to a constant in a given lexical scope, that constant is searched for in:
- Each entry in
Module.nesting
- Each entry in
Module.nesting.first.ancestors
- Each entry in
Object.ancestors
ifModule.nesting.first
is nil or a module.
关于Module.nesting
可以参考文档:http://ruby-doc.org/core-2.1.2/Module.html#method-c-nesting
翻译一下:
虽然汉语并不是一个精确的语言,但意境和直觉的表达,不论松本先生还是哪个程序猿,都是一样的。
1. 从当前作用域开始,逐层向上,寻找直接可视的目标
2. 从当前作用域的祖先们里寻找目标(2比1优先级低为什么?因为这点不符合第一直接可视要求,想想看很多的include进来的东西,哪里能看见呢!谁要是这么写程序,就是程序员公敌)
3. 寻找顶层作用域内的目标(这也符合直觉,因为顶层作用域是公用的,放最后也符合习惯;)
大大的缺陷
缺陷在第二步,从当前作用域的祖先们里寻找目标。
对一个程序员来说,最好的是能准确的写出自己用哪个名字空间内的哪个目标。
这个第二步中的默认规则显然违背了这个原理,因为如果我要用祖先的作用域内的目标,我可以写明使用这个目标,即多写几个字母,把目标的命名空间写上。
举例来说:
找A::B::C
就只应该找1. A::B空间内的C,或者3. 唯一默认空间内的::C
而不应该去找A::C,因为如果程序员想用A::C,则需要写明,只需要多写几个字母。
这个缺陷将会导致什么问题呢?
不明确的东西,就要有一个统一的默认值。而如果默认的寻找路径是多个,就必须保证不能交叉。否则就麻烦了。
找A::B::C,结果可能找到A::C和::C,这可不是我们愿意的,一旦程序大了,谁去理清楚这个玩意儿。最后还不是得程序员埋单,要么还得写清楚,要么就报错搞死人。
期待ruby后面改掉这个糟粕呀!!
测试代码改了下:
C = "At the top level" module A C = "In A" end module A module Y #none end end module A module B include A::Y module X end puts "----B----" puts "Module.nesting=#{Module.nesting.inspect}" # => [A::B, A] puts "Module.nesting.first=#{Module.nesting.first}" puts "Module.nesting.first.ancestors=#{Module.nesting.first.ancestors.inspect}" puts "Object.ancestors=#{Object.ancestors}" puts C # => "In A" puts "----B----" end end module A::B puts Module.nesting # => [A::B] puts C # => "At the top level" end
你要是能写对,可真不错。理解了这个,下面的问题才能更好理解。
----B----
Module.nesting=[A::B, A]
Module.nesting.first=A::B
Module.nesting.first.ancestors=[A::B, A::Y]
Object.ancestors=[Object, Kernel, BasicObject]
In A
----B----
A::B
At the top level
Process finished with exit code 0
Ruby's Autoload
http://fantaxy025025.iteye.com/blog/2098356 因为篇幅,拎出去写了分析,这里。
Rails Constant Autoloading
实现原理
使用了钩子方法:Module#const_missing
查找和加载步骤:
1. 使用ruby加载。
2. 如果报错,则用rails机制寻找并加载文件:MyModule::SomeClass # => my_module/some_class.rb
rails是跑在ruby上的。并没有修改ruby的寻找机制,只是出错了才处理加载,去加载常量对应的文件。
上文分析了ruby寻找常量时,在命名空间上一层一层的找法的缺陷。
rails也遵循了ruby的寻找方法,区别在于,rails会去加载对应的文件。
When Foo::Bar::Baz
is referred to, then, Rails will attempt to load the following constants in turn, until it finds one that is already loaded:
Foo::Bar::Baz | 对应autoload_paths/foo/bar/baz.rb
-
Foo::Baz
| 对应autoload_paths/foo/baz.rb -
Baz
| 对应autoload_paths/baz.rb
这样也会有加载的文件产生交叉的问题。
As soon as an already-loaded constant Baz
is encountered, Rails knows this cannot be the Baz
it is looking for, and the algorithm raises a NameError
.
这个是rails的机制,原因是什么?
因为ruby找不到的常量,rails只认为是因为没有加载对应的文件,其实这也是rails做的唯一工作。
而如果已经存在一个这样的常量了,而ruby找不到,那么rails也不能做太多的工作。
rails为什么不去掉中间这种不好的加载方法呢?
是可以去掉的。但很多人写的ruby代码,使用了2这种默认机制,如果去掉,就会导致自动加载不能用了。为了兼顾ruby,只能这样了。
Rails 自动加载的缺陷
# 同ruby一样,第二种加载容易导致交叉。
详细见上面分析。
# 依赖加载顺序
因为找不到相应的常量时,就会去找相应的文件。运行时,执行的不同顺序,会导致加载文件的顺序发生变化。这样可能引发很多问题,比如不同文件定义的常量同名了,但运行到不同的分支导致加载了不同的常量,这样的bug很难排查。
# 上下文信息丢失
ruby中的常量有多重,class, module, 字面常量比如Cache="MEM"这种。rails只收到const_missing,并没有收到上下文信息。
比如const_missing接收到Foo::Bar::Baz 这个常量有很多种表现形式,比如上面的简单的就3种了,还有不少变种,比如下面两种module的写法就是不同的,更别提module和class了。
module Foo module Bar #sth end end
module Foo::Bar #sth end end
class Foo class Bar #sth end end
rails接收不到上下文信息,所以导致rails只认为是第一种方式。
理解了文章的一些内容,再看 http://urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/
这个文章就好理解了。很多东西还是有其内在的原因的,死记只能解一时只用。
+
+
+
——
+
+
+