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

S.G*_*ami 5 ruby rubygems require

嗨,多年来我一直想知道为什么你不能使用这种Kernel#require方法来加载宝石。

例如,这将起作用:

#!/usr/bin/ruby -w
require 'ruby2d'    # => true
Run Code Online (Sandbox Code Playgroud)

这里 require 的所有者是 Kernel:

p Object.method(:require).owner    # => Kernel
p Kernel.method(:require).owner    # => #<Class:Kernel>
Run Code Online (Sandbox Code Playgroud)

但这有效:

p Object.send :require, 'ruby2d'    # => true
p String.send :require, 'ruby2d'    # => false
p Kernel.require 'ruby2d'           # => false
Run Code Online (Sandbox Code Playgroud)

或者

gem 'ruby2d'                        # => true
p String.send :require, 'ruby2d'    # => true
p Kernel.require 'ruby2d'           # => false
Run Code Online (Sandbox Code Playgroud)

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

不知何故,这不起作用:

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

这里发生了什么?

mat*_*att 6

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

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

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

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

接下来,我们可以看看原始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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

如果你跑

p Kernel.require 'ruby2d'
Run Code Online (Sandbox Code Playgroud)

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

p require 'ruby2d'
Run Code Online (Sandbox Code Playgroud)

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

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

这与我使用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)
Run Code Online (Sandbox Code Playgroud)

两者都是LoadError,但一个已经通过了 Rubygems,一个还没有。

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

gem 'ruby2d'
Kernel.require 'ruby2d'
Run Code Online (Sandbox Code Playgroud)

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