Ruby `require` 完全指南:从 `$LOAD_PATH` 到 `require_relative`

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 所在目录

它有几个优点:

  1. 不依赖工作目录:脚本从任何地方执行都能找到文件。
  2. 不污染 $LOAD_PATH :不需要为了加载项目内文件而往 $LOAD_PATH 里塞路径。
  3. 语义明确:一眼能看出是项目内相对引用。

返回值语义和 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

注意事项:

  1. 线程安全:多线程下同时首次引用同一常量,可能触发重复加载或竞争,老版本 Ruby 有已知问题。
  2. 文件必须定义对应常量 :否则会抛 NameError。
  3. Module#autoload 与 Kernel#autoload:前者是模块级,后者是顶层,作用域不同。
  4. 与 $LOADED_FEATURES 的关系 :autoload 触发加载后,文件会被加入 $LOADED_FEATURES(因为内部走 require 流程),所以之后再 require 同一文件会返回 false。
  5. 现代趋势 :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" 时:

  1. a 被标记为已加载,开始执行
  2. a 里 require_relative "b",b 被标记并执行
  3. b 里 require_relative "a",发现 a 已加载,返回 false,不重新执行
  4. b 继续执行 B = 2
  5. 回到 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 之前。


九、最佳实践

  1. 加载项目内文件,统一用 require_relative :不依赖工作目录,不污染 $LOAD_PATH。
  2. 加载 gem / 标准库,统一用 require:让 RubyGems 和 Bundler 处理路径。
  3. 不要往 $LOAD_PATH 里塞相对路径 :Ruby 3.1+ 会警告。用 File.expand_path(..., __dir__)。
  4. 不要写 require "./foo":依赖当前工作目录,脆弱。
  5. 文件顶部集中 require :便于一眼看出依赖,也避免运行时才发现 LoadError。
  6. 避免循环依赖 :循环 require 不报错,但会让常量加载顺序变得难以预测。用依赖注入、拆模块等方式消除。
  7. 不要滥用 autoload :现代项目用 Zeitwerk 或显式 require,可读性和可预测性更好。
  8. load 只用于确实需要重载的场景:比如开发环境的配置热更新。
  9. 注意大小写:写代码时严格匹配文件名,避免跨平台问题。

十、一句话总结

require 在 $LOAD_PATH 里找、只加载一次、自动补扩展名,用于 gem 和标准库;

require_relative 相对当前文件找,用于项目内文件;

load 每次都执行、不记录去重,但查找和扩展名补全机制与 require 相似,用于热重载;

autoload 懒加载常量,底层仍走 require,现代项目推荐用 Zeitwerk 管理;

理解 $LOAD_PATH 和 $LOADED_FEATURES,就理解了 Ruby 加载机制的全部。

相关推荐
htzyl2061 小时前
前端阶梯——第八章、CSS入门与选择器
前端·css
猪猪拆迁队1 小时前
跨电脑复制共享-WebRTC 打洞踩坑
前端·后端·go
Amos_Web1 小时前
Rspack 源码解析(十八):Tree Shaking 如何用 SideEffects 重写模块连接
前端·rust·前端框架
百度一下吧1 小时前
前端包管理工具和使用手册
前端
zhangzeyuaaa1 小时前
深入 Ruby:return 退出规则完全指南
开发语言·ruby
anxiao_m2 小时前
水利数字孪生怎么选?主流可视化渲染平台深度横向测评
大数据·前端·人工智能·图形渲染·云渲染
kill5222 小时前
useRef
前端·javascript·react.js
蓝悦无人机2 小时前
从“摸形状“到“点到线面的距离“——聊聊 SLAM 前端(激光雷达篇)
前端·loam·数据关联·激光slam·点云特征提取·点到线面距离·icp/ndt
flash俊杰3 小时前
工程约束怎么不拖后腿
前端