根据 rubyist 们的留言,做了两点补充:
详见文末。
作为 ruby 新手,对这 6 位容易拎不清,Google 全网,没有找到完整的对比,决定写一篇,希望能给同样迷惑的小伙伴一个参考。
关于这个问题,已经有很多大牛给到过解答,稍完整的比如:About Ruby require / load / autoload / include / extend,RubyChina 上也有很精彩的解答:基础 Ruby 中 Include, Extend, Load, Require 的使用区别,决定整理下各位大牛的解答,顺带加点个人的理解,对这 6 位进行下对比。
先给它们简单分类下:require,load, autoload 均涉及到文件的加载,归为一类,剩下的 include,prepend,extend 归为第二类。先来看第一类。
load
会每次都重新加载文件。autoload(const_name, 'file_path')
, 其中 const_name 通常是模块名,或者类名。看个例子来感受下三者的不同:【#= > 表示输出结果】
## 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 了一个 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 时,相当于打开了该类或者该对象的单件类,为其添加了单件方法。
比如:
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]
相比 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。
I strongly discourage the use of autoload in any standard libraries" (Re: autoload will be dead)
About Ruby require / load / autoload / include / extend
基础 Ruby 中 Include, Extend, Load, Require 的使用区别
When to use require
, load
or autoload
in Ruby?
第一次在 RubyChina 发帖,小激动~
前面已经提到在使用 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
具体有关单件类及祖先链部分,可以查阅《Ruby 元编程》(第 2 版)第五章,书中有非常详细的图解。
这个我就直接用书中的解说吧,参考来自《Ruby 元编程》(第 2 版)第二章。
用 true 是为了避免 load 带来的一个副作用。
load('file.rb')
加载进来的文件 file 中,如果含有常量,这些常量并不会像变量一样在加载完成后,落在当前作用域之外,所以这些常量就有可能会污染当前程序的命名空间,而使用load('file.rb', true)
,Ruby 会创建一个匿名空间,用它作为命名空间来容纳 file 中的所有常量,从而避免污染当前程序的命名空间。