Ruby 中的 autoload 方法

yufei       6 年, 2 月 前       1598

本章节我们来认识一下 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

从上面的结果中可以看出:

  1. 第一调用 autoload? :B 因为文件还未加载,所以返回类或模块所在的文件 ./b.rb
  2. 然后我们访问 B 模块触发模块的自动加载
  3. 第二次再调用 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 模块

目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.