为啥 Kernel#require 在 Ruby 中引发 LoadError?

Posted

技术标签:

【中文标题】为啥 Kernel#require 在 Ruby 中引发 LoadError?【英文标题】:Why does Kernel#require raise a LoadError in Ruby?为什么 Kernel#require 在 Ruby 中引发 LoadError? 【发布时间】:2019-12-05 16:43:59 【问题描述】:

您好多年来我一直想知道为什么您不能使用 Kernel#require 方法来加载宝石。

例如,这将起作用:

#!/usr/bin/ruby -w
require 'ruby2d'    # => true

这里 require 的所有者是 Kernel:

p Object.method(:require).owner    # => Kernel
p Kernel.method(:require).owner    # => #<Class:Kernel>

但这有效:

p Object.send :require, 'ruby2d'    # => true
p String.send :require, 'ruby2d'    # => false
p Kernel.require 'ruby2d'           # => false

gem 'ruby2d'                        # => true
p String.send :require, 'ruby2d'    # => true
p Kernel.require 'ruby2d'           # => false

[坏主意,但你可以在任何对象上发送 require 方法]

不知何故这不起作用:

#!/usr/bin/ruby -w
p Kernel.require 'ruby2d'
Traceback (most recent call last):
    1: from p.rb:2:in `<main>'
p.rb:2:in `require': cannot load such file -- ruby2d (LoadError)

这是怎么回事?

【问题讨论】:

【参考方案1】:

这里发生了一些事情并以有趣的方式进行交互,我们需要解开这些事情以了解正在发生的事情。

首先,require 的工作原理。有一个包含目录列表的全局变量$LOAD_PATHrequire 工作的“原始”方式(即没有 Rubygems)是 Ruby 将简单地在此列表中搜索所需文件,如果找到则加载它,否则会引发异常。

Rubygems 改变了这一点。当 Rubygems 被加载时,它是 replaces the built-in require method with its own,首先对原始文件进行别名。这个新方法首先调用原始文件,如果没有找到所需的文件,那么它不会立即引发异常,而是搜索已安装的 gem,如果找到匹配的文件,则该 gem 被激活。这意味着(除其他外)gem 的lib 目录被添加到$LOAD_PATH

尽管 Rubygems 现在是 Ruby 的一部分并默认安装,但它仍然是一个单独的库,并且原始代码仍然存在。 (您可以使用 --disable=gems 禁用加载 Rubygems)。

接下来我们可以看看原来的require方法是怎么定义的。它是done with the C function rb_define_global_function。这个函数又calls rb_define_module_function,那个函数looks like:

void
rb_define_module_function(VALUE module, const char *name, VALUE (*func)(ANYARGS), int argc)

    rb_define_private_method(module, name, func, argc);
    rb_define_singleton_method(module, name, func, argc);

如您所见,该方法最终被定义两次,一次是作为私有方法(即包含在 Object 中且随处可用的方法),一次是作为单例方法(即类方法)在Kernel上。

现在我们可以开始看看发生了什么。 Rubygems 代码仅替换包含的require 版本。当你调用 Kernel.require 时,你会得到原始的 require 方法,它对 Rubygems 一无所知。

如果你跑了

p Kernel.require 'ruby2d'

您将得到与禁用 Rubygems (ruby --disable=gems p.rb) 时运行以下相同的错误:

p require 'ruby2d'

在这两种情况下我都得到:

Traceback (most recent call last):
    1: from p.rb:1:in `<main>'
p.rb:1:in `require': cannot load such file -- ruby2d (LoadError)

这不同于我运行第二个示例 with Rubygems,在这种情况下我得到(因为我没有安装 gem):

Traceback (most recent call last):
    2: from p.rb:1:in `<main>'
    1: from /Users/matt/.rubies/ruby-2.6.1/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
/Users/matt/.rubies/ruby-2.6.1/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require': cannot load such file -- ruby2d (LoadError)

两个都是LoadErrors,但一个已经通过 Rubygems,一个没有。

Kernel.require 似乎工作的示例也可以解释,因为在这种情况下文件已经加载,而原始的 require 代码只是看到一个已经加载的文件并返回 false。 Kernel.require 也可以工作的另一个例子是

gem 'ruby2d'
Kernel.require 'ruby2d'

gem 方法会激活 gem,尽管它不会加载它。如上所述,这会将 gems lib 目录(包含作为 require 目标的文件)添加到 $LOAD_PATH,因此原始 require 代码将找到并加载它。

【讨论】:

我也许应该补充一点,你 require 是一个 file,而不是一个 gem。通常,gem 有一个与 gem 本身同名的“主”文件,并且 gem 在需要文件的同时被激活,因此这种区别通常会丢失,但对于理解最后一个示例很重要。跨度> 这就是为什么:Kernel.require 'bigdecimal' 返回true,而Kernel.require 'ruby2d' 将引发LoadError @S.Goswami 是的,bigdecimal 包含在 Ruby 中,并且已经在 LOAD_PATH 上。 (在我的机器上,它位于/Users/matt/.rubies/ruby-2.6.1/lib/ruby/2.6.0/x86_64-darwin17 中,位于LOAD_PATH 上)。您无需加载任何 gem 即可使用它。 你的答案是最好的。但只是举另一个例子$LOAD_PATH.map |x| Dir.children(x) if File.directory?(x) .flatten.compact.select |x| /webrick.*|bigdecimal.*|ruby2d.+/i === x 将返回["bigdecimal", "bigdecimal.rb", "webrick", "webrick.rb", "bigdecimal", "bigdecimal.so"] 并且它不会包括像'ruby2d'或'paint'或'rainbow'这样的宝石......所以Kernel.require只能加载那里的东西[正如你之前所说的] (甚至Kernel.require 'bigdecimal.so')运行正常!...

以上是关于为啥 Kernel#require 在 Ruby 中引发 LoadError?的主要内容,如果未能解决你的问题,请参考以下文章

kernel_require.rb:55:in `require': 无法加载此类文件错误

如何在 Ruby 中分析 `require` 语句?

Ruby require 'file' 不起作用,但 require './file' 可以。为啥?

Ruby的require相关知识

bundle install 的 gem 提示 cannot load such file

为啥这个 Ruby 方法通过引用传递它的参数