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

Lucia_ca · 2018年03月28日 · 最后由 xyy601 回复于 2019年09月14日 · 6772 次阅读
本帖已被设为精华帖!

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

补充:

根据 rubyist 们的留言,做了两点补充:

  • 使用 extend, require, include 时,module 在祖先链中的差异
  • 有关 load 中的参数 true

详见文末。


作为 ruby 新手,对这 6 位容易拎不清,Google 全网,没有找到完整的对比,决定写一篇,希望能给同样迷惑的小伙伴一个参考。

写在前面

关于这个问题,已经有很多大牛给到过解答,稍完整的比如: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。

参考:

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 发帖,小激动~


补充部分:

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

具体有关单件类及祖先链部分,可以查阅《Ruby 元编程》(第 2 版)第五章,书中有非常详细的图解。

有关 load 中的参数 true

这个我就直接用书中的解说吧,参考来自《Ruby 元编程》(第 2 版)第二章。

用 true 是为了避免 load 带来的一个副作用。

load('file.rb')加载进来的文件 file 中,如果含有常量,这些常量并不会像变量一样在加载完成后,落在当前作用域之外,所以这些常量就有可能会污染当前程序的命名空间,而使用load('file.rb', true),Ruby 会创建一个匿名空间,用它作为命名空间来容纳 file 中的所有常量,从而避免污染当前程序的命名空间。

第一个点赞,小激动~

不错不错,不过 ActiveSupport 里的 autoload 最终还是调用的 Ruby 自己的 autoload。他只是做了模块到文件名的转化,按照 Rails 的约束,只需要把类名或者模块名这一个参数传给 autoload,而不像 Ruby 本身的 autoload 需要传俩参数

吾辈认为,autoload 的废弃 实际上是 Ruby 有了成熟的包管理机制(融入了 gem)带来了模块管理就不需要了。现在的 Ruby 跟 Java 的包管理不相上下(当然还是有点差距)。

就像 Java 有了 lib 目录,当然后来加入了 maven / ant,Java 是一开始,这方面机制就比较完备的。

个人觉得 autoload 是解决 PHP 5 时代像:

require '../../module_a.php'

当时发明 autoload 还是以 “集中目录” autoload 一些模块:

function __autoload($classname) {
    $filename = "../../". $classname .".php";
    include_once($filename);
}

$obj = new ModuleA();

后来 autoload 已经远去,标准文档都建议改用 spl_autoload_register。

其实 composer 解决的就是跟 gem 同样目的的产物,但是,我觉得 PHP 的包管理没什么意义,作为标准有很多争议的语言(当时 CodeIgniter 有自己的 Code style,然后 Symphony 又有另外的还有锅铲各种更不用说了),如果不是 Laravel 带了个头,相信很多人是瞎写命名空间的。

说到这里提一点话外语,我觉得 PHP 作为 View 语言(像 Angular / React.js 的 View 部分语法作为刚出生定位)也差不多了,现在有点变态(比双性恋还扭曲的一种形态,不伦不类)了(首先不好好作为 View 展示,又去加入复杂的语法、没有一个标准的书写面孔,还有 Scalar 与 面向对象混乱的搭配,Scalar 崇尚的是内聚、无类型、效率,class 的加入就是类型独立、分裂、离散,两种简直就是混搭,如果开发者稍微不注意就会写出相爱相杀的代码)。

因此,用【编程语言界的畸形】来描述 PHP 最不为过。 人家 Perl 发明 $ 魔符是为了 跟 @(复数)还有 %(哈希)区分开来的,PHP 好歹参考人家 $,也得按人家规则来吧,你只搞一个 $ 然后数组还是 $var[] 然后 给数组追加语法又是 $var[] =,好,这是 “[]=” 的语法。

因此,我觉得 $ 完全在 PHP 变量名 中是负责卖萌的,不起任何作用。 要是我去重写它的 词法 phrase tokenizer,我一定会坚决删掉它。

呀,不小心黑了一把。。。表现出文不对题的尴尬

回头说 autoload,其实它就是脚本语言早期没有包(或命名空间)对已存在的模块的管理的一种应对措施。

对于猴子补丁啥的,我觉得应该说说 extend(允许类能够像 JS prototype 那样在 Ruby 的另一个叫做 singleton_methods 的继承支线)吧。

感谢 LZ 分享 Ruby 基本模块加载用法。

Peter 回复

哈哈,ruby China 上收到的第一个赞,小激动~

hegwin 回复

厉害!我看到它自己定义了一个 autoload 方法,但是没细看里面的代码, 再看,发现是覆写了 autoload, method 最后一句是

super const_name, path

这个应该就是调用了 ruby 本身的 autoload。给高手点赞!

jakit 回复

作为小白的我表示已经懵逼了,看不懂,要是我稍微懂点 PHP,应该能看懂点……受教了,感谢 PHP 大牛关于 autoload 的分享! 另,extend 确实会存在猴子补丁的情况,不过相近的 include,prepend 也会存在。Ruby 中没有 class_method, 它的类方法其实是单件方法,可以用类名调用 singleton_methods 来查看。

Lucia_ca 回复

噢,昨晚快睡着了,困困哒不小心搞错了,是 singleton_methods,已修正

jasl 将本帖设为了精华贴 03月29日 14:17

extendincludeprependload require autoload 是两套完全不同的东西

另外结合方法继承,prepend 可以用来实现 AOP 风格的编程 比如这样

jasl 回复

学习了。不是很懂 AOP 风格的编程,只知道 prepend 很强大,例子里面的输出结果,通过祖先链可以推断出来。 比如最后的那个 C A B 的输出。 Bar 的祖先链是

module Bmodule A class Bar,

调用 Bar.new.foo 时,沿着祖先链开始找 foo, 从 module B 开始,找到了 foo, 里面用了 super,也就是要调用它的上一个 module A 中的 foo, 而 module A 中的 foo method ,也使用了 super,沿着祖先链继续,A 的上一个是 class Bar,所以最后方法调用顺序会是:class Bar 的 foo, module A 中的 foo, module B 中的 foo, 假定 super 都是放在最后,调用的就会是 module B 中的 foo,module A 中的 foo,class Bar 的 foo,输出的会是 B A C。prepend 搭配 super 很强大,不过 super 的位置也挺重要的。

Lucia_ca 回复

面向切面编程,其他语言应用也挺广泛的

感谢分享,也顺便给我上了一课😀 。推荐看一下《元编程》这本书,可以加深对 Ruby 的理解,还能改善写作风格。一般 Ruby 里面实例方法会写成C#hello_method, 类方法会写成 C.hello_method ,模块嵌套 C::A::B。 这样 rubyist 看你的文章的时候会更有亲切感 (个人建议 😄 )。

jasl 回复

学习了😀

lanzhiheng 回复

感谢推荐,已经看过《元编程》,很精彩。建议很赞,代码风格还比较小白,会注意改进的 😀

jakit 回复

万事不忘黑 php,哈哈哈哈

关于 load 和 require 的补充

Speaking of Namespaces (23), there is one interesting detail that involves Namespaces, constants, and Ruby’s load and require methods. Imagine finding a motd.rb file on the web that displays a “message of the day” on the console. You want to add this code to your latest program, so you load the file to execute it and display the message:

load('motd.rb')

Using load, however, has a side effect. The motd.rb file probably defines variables and classes. Although variables fall out of scope when the file has finished loading, constants don’t. As a result, motd.rb can pollute your program with the names of its own constants—in particular, class names.

You can force motd.rb to keep its constants to itself by passing a second, optional argument to load:

load('motd.rb', true)

If you load a file this way, Ruby creates an anonymous module, uses that module as a Namespace to contain all the constants from motd.rb, and then destroys the module.

The require method is quite similar to load, but it’s meant for a different purpose. You use load to execute code, and you use require to import libraries. That’s why require has no second argument: those leftover class names are probably the reason why you imported the file in the first place. Also, that’s why require tries only once to load each file, while load executes the file again every time you call it.

很好,谢谢,把 include, extend, prepend 的使用在对象先祖链上的区别在补充一下就更好了

xyuchen 回复

这个提的好,好的,我晚点加上去

23楼 已删除
Lucia_ca 回复

楼主,最后一段 “有关 load 中的参数 true” 的描述没有看懂,能举个例子说明下带有参数 true 和不带参数 true 的区别吗?

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