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

Lucia_ca · 2018年03月28日 · 最后由 Awlter1 回复于 2018年04月19日 · 1799 次阅读
本帖已被设为精华帖!

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

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

共收到 19 条回复

第一个点赞,小激动~

不错不错,不过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.

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