读完 Ruby 元编程后的收获。
写在前面
关于这个问题,已经有很多大牛给到过解答,但是Google全网,并没有看到涉及这6个method的完整对比,
稍完整的比如:About Ruby require / load / autoload / include / extend,RubyChina上也有很精彩的解答:基础 Ruby 中 Include, Extend, Load, Require 的使用区别, 决定整理下各位大牛的解答,顺带加点个人的理解,对这6位进行下对比。
正文
先给它们简单分类下:require,load, autoload均涉及到文件的加载,归为一类,剩下的include,prepend,extend归为第二类。先来看第一类。
require
- kernel method,可以加载ruby文件,也可以加载外部的库。
- 相比load ,针对同一个文件,它只加载一次
load
- 与require很类似,但是
load
会每次都重新加载文件。 - 大部分情况下,除非你加载的库变动频繁,需要重新加载以获取最新版本,一般建议用require来代替load.
autoload
- 用法稍稍不同:
autoload(const_name, 'file_path')
, 其中const_name 通常是模块名,或者类名。 - 对于load和require,在ruby运行到require/load时,会立马加载文件,而autoload则只有当你调用module或者class时才会加载文件。
看个例子来感受下三者的不同:【#= > 表示输出结果】
## module_m.rb
module M
puts 'load a module'
class A
def self.hello
puts 'hello'
end
end
end
## test.rb
## require :只加载一次
puts "first load: #{(require './module_m.rb')}"
puts "load again: #{(require './module_m.rb')}"
#= > load a module
#= > first load: true
#= > load again: false
# load :多次加载
puts "first load: #{(load './module_m.rb')}"
puts "load again: #{(load './module_m.rb')}"
#= > load a module
#= > first load: true
#= > load a module
#= > load again: true
# autoload :调用时才加载
puts "first load: #{autoload(:M,'./module_m.rb')}"
puts "load again: #{autoload(:M,'./module_m.rb')}"
M::A::hello
#= > first load:
#= > load again:
#= > hello
不过现在应该很少有rubyist用autoload了。
2011年,Matz针对 Autoload will be dead,有如下的声明:
至于原因,则是autoload本身在多线程环境下存在基本的缺陷,这个我并没有尝试过,不是很理解。stack overflow上When to use require
, load
or autoload
in Ruby?有位是这么说的:
The lazyness of autoload sounds nice in theory, but many Ruby modules do things like monkey-patching other classes, which means that the behavior of unrelated parts of your program may depend on whether a given class has been used yet or not。
他提到了猴子补丁的情况。如果有例子,应该会更好理解。
顺带提一句,Rails的ActiveRecord中大量使用的autoload,跟这里的autoload不是一回事,它是module ActiveSupport::Autoload中的方法。
include
当一个类或者模块 include了一个module M时, 则该类或者模块就拥有了该module M的方法。
当涉及多个类调用同一方法时,这个方法就可以抽离出来,放入module中,然后类只需include该module即可。这样的做法也正体现了DRY原则。
例如:
module M
def my_method; puts "hello"; end
end
class C
include M
end
class D
include M
end
C.new.my_method #= > hello
D.new.my_method #= > hello
include的另一种较常见的用法是搭配extend,实现包含并扩展类的功能,同时可能还会搭配着钩子方法included。在一些常用gem的源代码中,可以看到这类用法的身影。
extend
当一个类或者对象使用extend 时,相当于打开了该类或者该对象的单件类,为其添加了单件方法。
比如:
module MyModule
def a_method; puts "hello"; end
end
class C
extend MyModule
end
obj = []
obj.extend MyModule
C.a_method #= > hello
C.singleton_methods #= > [:a_method]
obj.a_method #= > hello
obj.singleton_methods #= > [:a_method]
使用include实现同样的效果:
module MyModule
def a_method; puts "hello"; end
end
class C
class << self
include MyModule
end
end
obj = []
class << obj
include MyModule
end
C.a_method #= > hello
C.singleton_methods #= > [:a_method]
obj.a_method #= > hello
obj.singleton_methods #= > [:a_method]
prepend
相比include,extend, prepend「Available since Ruby 2」的知名度和使用率要少很多。
prepend和include很像,当一个类prepend或include 一个模块时,该模块中的方法会成为该类的实例方法。
二者的区别在于,模块在祖先链中的位置。 使用include时,模块在包含它的类之上。如果是prepend, 则是在prepend它的类之下。而祖先链中位置的不同,决定了方法调用的顺序。
比如下面这个例子:
module M1
def hello
puts "hello! this is module M1"
end
end
module M2
def hello
puts "hello! this is module M2"
end
end
class C
prepend M1
include M2
def hello
puts "hello! this is class C"
end
end
C.ancestors #=> [M1, C, M2, Object, Kernel, BasicObject]
C.new.hello #=> hello! this is module M1
这里,祖先链的顺序是M1在最前面,所以即使C中定义了一个method hello, 也不会被调用,因为module M1覆写了这个method。
从上面的例子也可以看出,prepend是很方便的方法包装器,假定我们想要给class C的hello method添加一些其他的功能实现,则可以这样写:
module M1
def hello
puts "add something outside C#hello"
super
end
end
...... # 省略module M2
class C
prepend M1
include M2
def hello
puts "hello! this is class C"
end
end
C.new.hello
#=> add something outside C#hello
#=> hello! this is class C
在module M1中覆写了hello,同时使用了super,调用了C中原来的hello method。
extend, require, include中,module在祖先链中的差异
前面已经提到在使用include时,模块在包含它的类之上。如果是prepend, 则是在prepend它的类之下。那么使用extend,模块会出现在哪里?
根据之前的例子,改编了下:
module M1
def hello
puts "hello! this is module M1::hello"
end
end
module M2
def hello
puts "hello! this is module M2"
end
end
## 添加 M3
module M3
def hello
puts "hello! this is module M3"
end
end
class C
prepend M1
include M2
extend M3
def hello
puts "hello! this is C#hello"
end
end
C.ancestors #=> [M1, C, M2, Object, Kernel, BasicObject]
此时,C的祖先链中并没有出现M3,那么M3在哪里?
当类extend某个module时,其实是扩展了该类的类方法,所以,可以在该类的单件类的祖先链里面找找。
承接上面的例子, 查看C单件类的祖先链:
C.singleton_class.ancestors
#=> [#<Class:C>, M3, #<Class:Object>,#<Class:BasicObject>,Class, Module, Object, Kernel,BasicObject]
可以看到,M3在该类的单件类的上方。此时调用C.hello, 会得到
C.hello #=> hello! this is module M3
当然,如果你在C中定义了类方法hello,则会调用C自定义的这个类方法,比如:
...... # 省略module M1 M2 M3
class C
...... # 同上,省略
def self.hello
puts "hello! This is C.hello"
end
end
C.hello #=> hello! This is C.hello
如果想要调用M3中的hello,在C的hello中加上super即可。
...... # 省略module M1 M2 M3
class C
...... # 同上,省略
def self.hello
puts "hello! This is C.hello"
super
end
end
C.hello
#=> hello! This is C.hello
#=> hello! this is module M3
参考
I strongly discourage the use of autoload in any standard libraries” (Re: autoload will be dead)
About Ruby require / load / autoload / include / extend