Ruby中require,load,autoload,extend,include,prepend的区别

读完 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

基础 Ruby 中 Include, Extend, Load, Require 的使用区别

When to use require, load or autoload in Ruby?