Ruby require 完全指南:从 $LOAD_PATH 到 require_relative
require 是 Ruby 里最常用、也最容易被忽略细节的方法之一。很多人写了几年 Ruby,仍然会在 require 和 require_relative 之间犹豫,或者被 LoadError 卡住半天。这篇文章把加载机制讲透:查找路径、去重原理、四种加载方式的区别、autoload、循环依赖、常见坑点与最佳实践。
一、四个加载方法总览
Ruby 中和加载文件相关的方法主要有四个:
| 方法 | 查找范围 | 重复加载 | 需要扩展名 | 典型场景 |
|---|---|---|---|---|
require |
$LOAD_PATH |
不会 | 自动补全 | 加载 gem、标准库 |
require_relative |
当前文件所在目录 | 不会 | 自动补全 | 加载项目内文件 |
load |
$LOAD_PATH(简单文件名)/ 给定路径(推荐绝对路径) |
每次都会 | 自动补全(与 require 相同机制) |
开发时热重载脚本 |
autoload |
$LOAD_PATH |
不会 | 自动补全 | 懒加载常量 |
一句话原则:
加载 gem / 标准库用
require;加载项目内文件用require_relative;需要反复执行用load。
需要特别注意的是:load 和 require 在文件查找层面共享相似的底层机制(rb_find_file),都会搜索 $LOAD_PATH 并尝试补全扩展名。二者的核心区别在于 去重 :require 会记录到 $LOADED_FEATURES,只加载一次;load 不记录,每次调用都重新执行。
二、require 详解
1. 查找路径:$LOAD_PATH
require "foo" 不会在当前目录找文件,而是在 $LOAD_PATH (别名 $:)里逐个目录查找。
ruby
$LOAD_PATH # => ["/usr/lib/ruby/3.2.0", "/usr/lib/ruby/3.2.0/x86_64-linux", ...]
$: # 同一个对象,别名
$LOAD_PATH 的组成可分为两部分:
默认包含:
- Ruby 核心库目录
site_ruby、vendor_ruby- RubyGems 激活后的 gem 的
lib目录
可通过以下方式扩展:
- 命令行
-I指定的目录 - 环境变量
RUBYLIB指定的目录
注意:.(当前目录)默认不在 $LOAD_PATH 里 (Ruby 1.9.2 起正式移除;1.9.1 中仍包含 .)。所以:
ruby
require "foo" # 找不到当前目录的 foo.rb
require "./foo" # 可以,显式相对路径
但 require "./foo" 依赖当前工作目录,脚本从别处执行就会失效,所以不推荐。
2. 扩展名解析
require "foo" 不需要写 .rb。Ruby 会依次尝试补全:
foo.rb- 原生扩展:
foo.so(Linux)、foo.bundle(macOS)、foo.dll(Windows)
ruby
require "json" # 实际加载 json.rb(或 json/ext/...)
require "set" # 实际加载 set.rb
如果显式写了扩展名,Ruby 就只找那个文件名:
ruby
require "foo.rb" # 只找 foo.rb
3. 返回值
- 成功加载:返回
true - 已经加载过:返回
false(不重复执行) - 找不到文件:抛出
LoadError
ruby
require "set" # => true
require "set" # => false
require "no_such_file" # LoadError
这个返回值常被用来做条件加载:
ruby
begin
require "nokogiri"
rescue LoadError
warn "nokogiri 未安装,功能降级"
end
4. 去重机制:$LOADED_FEATURES
Ruby 用一个全局数组记录所有已加载的文件:
ruby
$LOADED_FEATURES # 别名 $"
里面存的是解析后的绝对路径,例如:
ruby
require "set"
$LOADED_FEATURES.grep(/set\.rb/)
# => ["/usr/lib/ruby/3.2.0/set.rb"]
require 每次会先把目标路径解析成绝对路径,再检查是否已在 $LOADED_FEATURES 中。已存在就返回 false,不执行。
⚠️ 符号链接陷阱 :如果同一个文件通过两个不同的符号链接路径被 require,可能被加载两次。历史上这是个经典问题,实践中要避免同一文件有多个入口路径。
三、require_relative
require_relative 相对于当前文件所在目录 解析路径,不依赖 $LOAD_PATH,也不依赖当前工作目录。
ruby
# 文件结构:
# app/
# main.rb
# lib/
# helper.rb
# app/main.rb
require_relative "lib/helper" # 相对于 app/main.rb 所在目录
它有几个优点:
- 不依赖工作目录:脚本从任何地方执行都能找到文件。
- 不污染
$LOAD_PATH:不需要为了加载项目内文件而往$LOAD_PATH里塞路径。 - 语义明确:一眼能看出是项目内相对引用。
返回值语义和 require 一样:true / false / LoadError。
require_relative 的路径同样会被记录到 $LOADED_FEATURES,与 require 共享去重机制。也就是说,require_relative "foo" 之后,再 require "foo"(如果路径解析后指向同一绝对路径)会返回 false,不会重复加载。
在 Ruby 2.0+ 中,可以用 __dir__ 配合 require 达到类似效果,但 require_relative 更简洁:
ruby
# 等价写法,但更啰嗦
require File.expand_path("lib/helper", __dir__)
# 推荐
require_relative "lib/helper"
四、load
load 和 require 有几个关键区别:
| 特性 | require |
load |
|---|---|---|
| 重复加载 | 只加载一次 | 每次调用都重新执行 |
| 查找路径 | $LOAD_PATH |
$LOAD_PATH(简单文件名)/ 给定路径(推荐绝对路径) |
| 扩展名 | 自动补全 | 自动补全(与 require 相同机制) |
| 返回值 | true / false |
true(或抛 LoadError) |
记录到 $LOADED_FEATURES |
是 | 否 |
ruby
load "./config.rb" # 每次都重新执行
load "./config.rb" # 再执行一次
load 的典型用途:
- 开发环境热重载配置文件
- 执行一次性脚本
- 测试时反复加载被测文件
load 还有一个冷门参数 wrap:
ruby
load "foo.rb", true # 在匿名模块中执行,隔离命名空间
当 wrap=true 时,脚本在一个匿名模块的实例上下文中执行,self 不再是 main(顶层 Object 实例),而是一个匿名模块实例。这可以保护调用方的全局命名空间,避免加载文件中的方法、常量污染当前作用域。很少用到,但理解其作用有助于排查命名冲突。
五、$LOAD_PATH 与 $LOADED_FEATURES
1. 操作 $LOAD_PATH
ruby
$LOAD_PATH.unshift("/my/lib") # 加到最前面,优先级最高
$LOAD_PATH.push("/my/lib") # 加到最后
$LOAD_PATH << "/my/lib" # 同上
$LOAD_PATH.delete("/my/lib")
$LOAD_PATH.include?("/my/lib")
命令行方式:
bash
ruby -I./lib main.rb
# 或
RUBYLIB=./lib ruby main.rb
⚠️ Ruby 3.1+ 对 $LOAD_PATH 中的相对路径发出弃用警告。官方建议始终使用绝对路径:
ruby
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
2. 查看已加载文件
ruby
$LOADED_FEATURES.grep(/rails/)
调试「为什么这个文件没被加载」或「为什么被加载了两次」时非常有用。
六、autoload:懒加载
autoload 注册一个常量,当该常量第一次被引用时才加载对应文件:
ruby
# lib/my_app.rb
module MyApp
autoload :User, "my_app/user"
autoload :Post, "my_app/post"
end
MyApp::User.new # 此时才 require "my_app/user"
autoload? 可以查询是否注册了某个常量的懒加载:
ruby
MyApp.autoload?(:User) # => "my_app/user"
MyApp.autoload?(:Foo) # => nil
注意事项:
- 线程安全:多线程下同时首次引用同一常量,可能触发重复加载或竞争,老版本 Ruby 有已知问题。
- 文件必须定义对应常量 :否则会抛
NameError。 Module#autoload与Kernel#autoload:前者是模块级,后者是顶层,作用域不同。- 与
$LOADED_FEATURES的关系 :autoload触发加载后,文件会被加入$LOADED_FEATURES(因为内部走require流程),所以之后再require同一文件会返回false。 - 现代趋势 :Zeitwerk(Rails 6+ 默认加载器)用文件约定和自动化加载替代了手写
autoload调用,底层仍使用 Ruby 的autoload机制(配合绝对路径和Kernel#require的装饰)。新项目推荐使用 Zeitwerk,而不是手动管理autoload。
七、循环 require
Ruby 会把文件加入 $LOADED_FEATURES 早于 执行文件内容。所以循环 require 不会无限递归:
ruby
# a.rb
require_relative "b"
A = 1
# b.rb
require_relative "a"
B = 2
执行 require_relative "a" 时:
a被标记为已加载,开始执行a里require_relative "b",b被标记并执行b里require_relative "a",发现a已加载,返回false,不重新执行b继续执行B = 2- 回到
a,执行A = 1
但如果 b.rb 在加载时就引用 A:
ruby
# b.rb
require_relative "a"
puts A # NameError!此时 A 还没定义
根本原因是:a.rb 虽然已经被加入 $LOADED_FEATURES,但它的执行流还没走到 A = 1 那一行就被 b 打断了。b 中 require_relative "a" 返回 false 后继续执行,此时 A 尚未定义,自然报 NameError。
解决办法 :把常量定义放在 require 之前,或者避免循环依赖(更好的做法)。
八、常见坑
坑1:require 找不到当前目录的文件
ruby
# main.rb
require "helper" # LoadError
. 不在 $LOAD_PATH 里。用 require_relative "helper" 或 require "./helper"。
坑2:require_relative 在 eval / instance_eval 中行为异常
require_relative 依赖 __FILE__,在动态生成的代码里 __FILE__ 可能是 (eval) 或 (irb),导致解析错误。这类场景改用 require + 绝对路径。
坑3:require 路径大小写敏感
macOS / Windows 默认文件系统不区分大小写,Linux 区分。require "Foo" 在本地能跑,上服务器就 LoadError。严格按文件名大小写写。
坑4:gem 未安装时的 LoadError
require "nokogiri" 报 LoadError 时,先确认:
bash
gem list nokogiri
bundle exec ruby main.rb
bundle exec 会限制可用的 gem 版本,本地能跑、bundle exec 报错,通常是 Gemfile 里没写。
坑5:load 不会去重,容易重复定义
ruby
load "config.rb"
load "config.rb" # 常量重复赋值警告,方法重复定义
load 是执行脚本,不是引入模块。
坑6:修改 $LOAD_PATH 后没生效
require 是在调用时查 $LOAD_PATH,所以运行时修改是有效的。但要确保修改发生在 require 之前。
九、最佳实践
- 加载项目内文件,统一用
require_relative:不依赖工作目录,不污染$LOAD_PATH。 - 加载 gem / 标准库,统一用
require:让 RubyGems 和 Bundler 处理路径。 - 不要往
$LOAD_PATH里塞相对路径 :Ruby 3.1+ 会警告。用File.expand_path(..., __dir__)。 - 不要写
require "./foo":依赖当前工作目录,脆弱。 - 文件顶部集中
require:便于一眼看出依赖,也避免运行时才发现LoadError。 - 避免循环依赖 :循环
require不报错,但会让常量加载顺序变得难以预测。用依赖注入、拆模块等方式消除。 - 不要滥用
autoload:现代项目用 Zeitwerk 或显式require,可读性和可预测性更好。 load只用于确实需要重载的场景:比如开发环境的配置热更新。- 注意大小写:写代码时严格匹配文件名,避免跨平台问题。
十、一句话总结
require在$LOAD_PATH里找、只加载一次、自动补扩展名,用于 gem 和标准库;
require_relative相对当前文件找,用于项目内文件;
load每次都执行、不记录去重,但查找和扩展名补全机制与require相似,用于热重载;
autoload懒加载常量,底层仍走require,现代项目推荐用 Zeitwerk 管理;理解
$LOAD_PATH和$LOADED_FEATURES,就理解了 Ruby 加载机制的全部。