本章节我们来认识一下 Ruby 中的 autoload
方法,主要讲解以下几个内容:
- 自动加载注册和延迟加载
Module#autoload?
方法Module#autoload
方法背后的逻辑
如果你对
require
方法不熟悉,请先访问我们的另一篇文章 Ruby 中的 require 和 require_relative 方法
自动加载注册和延迟加载
一般情况下,在 Ruby 中, 我们使用 require
方法来加载模块。但 require
方法有一个缺点,就是无论接下来的代码中是否会用到被加载的模块,该模块都会在调用 require
方法时立即被加载。这可能会导致不必要的性能损耗,因为我们接下来的代码中可能根本不会用到 b.rb
中的类或模块。
假设当前目录下存在一个文件 b.rb
,该文件中定义了一个模块 B
b.rb
module B puts 'The module B is loading!' end
那么,如果我们使用 require
方法加载这个文件,比如下面的 demo.rb
代码,每次运行都会加载 b.rb
demo.rb
module Demo puts "The B module isn't yet loaded!" require './b.rb' puts "The B module has been successfully loaded!" end
运行上面的代码,输出结果如下
[root@www.twle.cn ruby]# ruby demo.rb The B module isn't yet loaded! The module B is loading! The B module has been successfully loaded!
这可能不是我们想要的,我们想要的可能是只有在访问 b.rb
中的 B
模块时才自动加载这个文件。这种加载方法叫做 延迟加载。
延迟加载 白话点说,就是提前先告诉程序,我们可能需要用到某个文件中的模块或者类,但程序你不要事先加载文件,只要等到我第一次使用文件中的模块或类时再加载就可以了。
Ruby 提供了 autoload()
方法来实现延迟加载,该方法的原型如下
autoload(module, filename) → nil
参数 | 说明 |
---|---|
module | 要加载的模块或类 |
filename | 模块或类所在的文件 |
autoload
方法并不会立即加载 filename
文件,而是在第一次使用 module
参数的模块或类时才开始加载。
我们使用 autoload
方法对 demo.rb
进行改造下,演示下 autoload
方法的机制
demo.rb
module Demo autoload(:B, './b.rb') puts "The Engine module isn't yet loaded!" B puts "The Engine module has been successfully loaded!" end
运行上面的代码,输出结果如下
[root@www.twle.cn ruby]# ruby demo.rb The B module isn't yet loaded! The module B is loading! The B module has been successfully loaded!
上面的 demo.rb
文件中,虽然我们在模块的开始就使用 autoload(:B, './b.rb')
加载了 b.rb
文件,但它没有立即被加载,而是等到我们调用了该文件中的 B
模块时,才开始加载。
Module#autoload?
方法
延迟加载 有很大的好处,但是坏处也不是没有: 延迟加载也是加载,加载就有可能失败。
这就会导致使用时模块或这类根本就不存在。
那么,我们就要一种机制来保证使用时的模块或类存在,简单来说,就是检查延时加载是否加载成功。
好在 Ruby 已经帮我们考虑到了这一点。提供了 Module#autoload?
方法。该方法的原型如下
autoload?(name) → String or nil
Module#autoload?
方法用于检查特定命名空间中已注册的模块/类是否已加载。该方法只有一个参数
|参数|说明| |name|要检查的命名空间下的模块或类|
如果要检查的模块或类已经加载,那么该方法返回模块或类所在的文件,如果已经存在,则返回 nil
。从某些方面说,该方法的结果真是反人类,如果未加载则返回需要加载的文件名,如果已经加载则返回 nil
。
你说,气不气人,但这是一个大坑,大坑啊。
从某些方面来说,该方法还用于检查模块是否在特定命名空间中作为自动加载注册(或不注册)。
需要注意的是,
autoload?
方法并不会触发模块的加载,它很单纯,只用于检查。
我们写一段代码来演示下 Module#autoload?
方法
demo.rb
module Demo autoload(:B, './b.rb') p autoload?(:B) B p autoload?(:B) end
运行上面的代码,输出结果如下
[root@www.twle.cn ruby]# ruby demo.rb "./b.rb" The module B is loading! nil
从上面的结果中可以看出:
- 第一调用
autoload? :B
因为文件还未加载,所以返回类或模块所在的文件./b.rb
- 然后我们访问
B
模块触发模块的自动加载 - 第二次再调用
autoload? :B
因为文件已经加载,所以返回nil
当然了,我们还可以对示例代码做一些改变,把 autoload(:B, './b.rb')
放在模块之外,如下面的代码
demo.rb
autoload(:B, './b.rb') module Demo p "enter Demo module" p autoload? :B p "out Demo module" end p autoload? :B
运行结果如下
[root@www.twle.cn ruby]# ruby demo.rb "enter Demo module" nil "out Demo module" "./b.rb"
结果是不是让你大跌眼镜!!! 为啥 Demo
模块中的 p autoload? :B
返回会是 nil
原因竟然是 autoload(:C, './c.rb')
并不是在 Demo
模块中添加的自动加载,而是在顶级命名空间中添加的自动加载。
而第二次使用 p autoload? :B
,因为和 autoload(:C, './c.rb')
同处于一个命名空间之下,所以能正确的返回值。
是不是很气人,很气人,再也不相信人生的既视感!
Module#autoload
背后的逻辑
好了,现在我们对 Ruby 中的延时加载已经有所了解。你是不是很想了解,这个气死人的 autoload?
方法到底是怎么实现的。
而实现原理很简单,在特定名称空间中调用 Module#autoload
方法时,模块/类和作为方法参数给出的文件路径存储在内部哈希表 constants
中。
我们写一段代码来演示下
demo.rb
module Demo autoload(:B, './b.rb') p constants end
运行结果如下
[root@www.twle.cn ruby]# ruby demo.rb [:B]
从输出的结果中可以看出,即使模块 B
未加载,也会存在 constants
常量中。
实际上,对 autoload
的调用将自动创建一个名为该方法的第一个参数的常量,并标记为 autoload registered
。此常量的值将是 undefined
而不是 nil
当调用 B
模块时, Ruby 将在 A
命名空间的 constants
哈希表中搜索 B
条目
然后使用 Kernel#require
方法加载文件
最后,常量哈希表将自动加载 B
模块