为什么Ruby中的Kernel#require会引发LoadError?

5

嗨,多年来我一直想知道为什么不能使用Kernel#require方法来加载gem包。

例如,以下代码可以正常工作:

#!/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个回答

6
这里有几件事情正在交互,并以有趣的方式进行,我们需要拆开来理解发生了什么。
首先是 require 的工作原理。存在一个全局变量 $LOAD_PATH,其中包含一个目录列表。最初的 require 工作方式(即没有 Rubygems)是,Ruby会简单地在此列表中搜索所需文件,如果找到则加载它,否则将引发异常。
Rubygems 更改了这一点。当 Rubygems 被加载时,它替换了内置的require方法为自己的方法,并将原始方法别名化。这种新方法首先调用原始方法,如果找不到所需的文件,则不会立即引发异常,而是搜索已安装的 gem,并且如果找到匹配的文件,则激活该 gem。这意味着(除其他外)gem 的 lib 目录被添加到 $LOAD_PATH 中。
即使 Rubygems 现在成为 Ruby 的一部分并默认安装,但它仍然是一个单独的库,原始代码仍然存在。(您可以使用--disable=gems禁用 Rubygems 的加载)。
接下来,我们可以看看如何定义原始的 require 方法。它是用 C 函数rb_define_global_function 完成的。该函数反过来又调用了rb_define_module_function,而该函数则类似于这样
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 时,您将得到不知道任何关于 Rubygems 的原始 require 方法。
如果您运行
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)

这与我使用Rubygems运行第二个示例时不同,因为我没有安装宝石,所以会得到以下结果:
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)

都是 LoadError,但一个经过了 Rubygems 的处理,而另一个没有。

在那些 Kernel.require 看似起作用的例子中,也可以解释,因为在这些情况下,文件已经被加载过了,原始的 require 代码只是看到了一个已经加载的文件,并返回 false。另一个 Kernel.require 能够工作的例子是

gem 'ruby2d'
Kernel.require 'ruby2d'
gem方法激活宝石,但不加载它。 如上所述,这将添加宝石的lib目录(包含要求目标文件)到$LOAD_PATH,因此原始的require代码将找到并加载它。

我或许应该补充一下,你需要的是一个“文件”,而不是一个“gem”。通常情况下,一个gem有一个与其同名的“main”文件,并且在文件被require时激活了这个gem,所以这个区别经常被忽略,但对于理解最后一个例子非常重要。 - matt
这就是为什么:Kernel.require 'bigdecimal' 返回 true,而 Kernel.require 'ruby2d' 会抛出 LoadError 的原因。 - 15 Volts
1
@S.Goswami 是的,bigdecimal 已经包含在 Ruby 中,并且已经在 LOAD_PATH 上了。(在我的机器上,它位于 /Users/matt/.rubies/ruby-2.6.1/lib/ruby/2.6.0/x86_64-darwin17,这个路径已经在 LOAD_PATH 上了)。你不需要加载任何 gem 就可以使用它。 - matt
你的答案是最好的。但是为了举另一个例子,$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' 这样的 gem... 因此,Kernel.require 只能加载已经存在的内容(就像你之前说的那样),即使 Kernel.require 'bigdecimal.so' 也可以正常运行!... - 15 Volts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接