【问题标题】:How can I add a method to the global scope in Ruby?如何在 Ruby 中将方法添加到全局范围?
【发布时间】:2023-03-16 03:19:01
【问题描述】:

Sinatra 定义了许多似乎存在于当前范围内的方法,即不在类声明中。这些在 Sinatra gem 中定义。

我希望能够编写一个 gem 来创建一个我可以从全局范围调用的函数,例如

add_blog(:my_blog)

这将在全局范围内调用函数 my_blog。

显然,我可以使用 add_blog 函数对 gem 中的 Object 类进行猴子补丁,但这似乎有点过头了,因为它会扩展每个对象。

【问题讨论】:

标签: ruby


【解决方案1】:

TL;DR extend-ing 顶层的模块将其方法添加到顶层而不将它们添加到每个对象。

有三种方法可以做到这一点:

选项 1:在 gem 中编写顶级方法

#my_gem.rb

def add_blog(blog_name)
    puts "Adding blog #{blog_name}..."
end
#some_app.rb

require 'my_gem' #Assume your gem is in $LOAD_PATH
add_blog 'Super simple blog!'

这会起作用,但它不是最简洁的方法:如果不将方法添加到顶层,就不可能要求您的 gem。有些用户可能想在没有这个的情况下使用您的 gem。

理想情况下,根据用户的偏好,我们可以通过某种方式使其在范围内或在顶层可用。为此,我们将在模块中定义您的方法:

#my_gem.rb
module MyGem

    #We will add methods in this module to the top-level scope
    module TopLevel
        def self.add_blog(blog_name)
            puts "Adding blog #{blog_name}..."
        end
    end

    #We could also add other, non-top-level methods or classes here
end

现在我们的代码的范围很好。问题是我们如何让它从顶层访问,所以我们并不总是需要调用 MyGem::TopLevel.add_blog

让我们看看 Ruby 中的 顶级 的真正含义。 Ruby 是一种高度面向对象的语言。这意味着,除其他外,所有方法都绑定到一个对象。当您调用像 putsrequire 这样明显的全局方法时,实际上是在调用 a "default" object called main 上的方法。

因此,如果我们想将一个方法添加到顶级作用域,我们需要将它添加到main。我们有几种方法可以做到这一点。

选项 2:向对象添加方法

main 是类Object 的一个实例。如果我们将模块中的方法添加到Object(OP 所指的monkey patch),我们将能够从 main 以及顶层使用它们。我们可以将我们的模块用作mixin

#my_gem.rb

module MyGem
    module TopLevel
        def self.add_blog
            #...
        end
    end
end

class Object
    include MyGem::TopLevel
end

我们现在可以从顶层调用 add_blog。但是,这也不是一个完美的解决方案(正如 OP 指出的那样),因为我们不仅将新方法添加到 main,我们还将它们添加到 @ 的 每个 实例987654341@!不幸的是,几乎 Ruby 中的所有内容都是 Object 的后代,所以我们刚刚让 add_blog 可以在任何东西上调用!例如,1.add_blog("what does it mean to add a blog to a number?!")

这显然是不可取的,但在概念上非常接近我们想要的。让我们对其进行改进,以便我们可以仅向 main 添加方法。

选项 3:仅向 main 添加方法

那么如果include 将模块中的方法添加到类中,我们可以直接在 main 上调用它吗?请记住,如果在没有明确接收者(“拥有对象”)的情况下调用方法,它将在 main 上调用。

#app.rb

require 'my_gem'
include MyGem::TopLevel

add_blog "it works!"

看起来很有希望,但仍然不完美——事实证明include 将方法添加到接收器的类中,而不仅仅是接收器,所以我们仍然可以做一些奇怪的事情,比如1.add_blog("still not a good thing!")

所以,为了解决这个问题,我们需要一个方法,它只向接收对象添加方法,而不是它的类。 extend 就是那个方法。

此版本将我们的 gem 的方法添加到顶级范围,而不会弄乱其他对象:

#app.rb

require 'my_gem'
extend MyGem::TopLevel

add_blog "it works!"
1.add_blog "this will throw an exception!"

太棒了!最后一个阶段是设置我们的 Gem 以便用户可以将我们的顶级方法添加到 main 中,而无需自己调用 extend。我们还应该为用户提供一种在完全范围内使用我们的方法的方法。

#my_gem/core.rb

module MyGem
    module TopLevel
        def self.add_blog...

    end
end


#my_gem.rb

require './my_gem/core.rb'
extend MyGem::TopLevel

这样用户可以将您的方法自动添加到顶层:

require 'my_gem'
add_blog "Super simple!"

或者可以选择以作用域的方式访问方法:

require 'my_gem/core'
MyGem::TopLevel.add_blog "More typing, but more structure"

Ruby 通过一种叫做the eigenclass 的魔法实现了这一点。每个 Ruby 对象,以及作为类的实例,都有自己的特殊类——它的特征类。我们实际上是使用extendMyGem::TopLevel 方法添加到main 的特征类中。


这是我将使用的解决方案。这也是Sinatra 使用的模式。 Sinatra 以稍微复杂的方式应用它,但本质上是相同的:

当你打电话时

require 'sinatra'

sinatra.rb 有效地添加到您的脚本中。该文件调用

require sinatra/main

sinatra/main.rb 来电

extend Sinatra::Delegator

Sinatra::Delegator 相当于我上面描述的 MyGem::TopLevel 模块 - 它让 main 了解 Sinatra 特定的方法。

Delegator 略有不同的地方 - Delegator 不是直接将其方法添加到 main 中,而是让 main 将特定方法传递给指定的目标类 - 在本例中为 Sinatra::Application

您可以在 Sinatra 代码中亲自看到这一点。对sinatra/base.rb 的搜索表明,当Delegator 模块被扩展或包含时,调用范围(在本例中为“main”对象)会将以下方法委托给Sinatra::Application

:get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
:template, :layout, :before, :after, :error, :not_found, :configure,
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
:production?, :helpers, :settings, :register

【讨论】:

  • 因此方法是在一个模块中定义的(在这种情况下称为 Delegator,它只是委托给 Application)。然后在全局上下文中的某个地方,我们扩展了将这些方法添加到 main 的模块。
  • 我认为这是一个很好的解释,欢迎来到 Stack Overflow!
  • 谢谢!我用一些 Ruby 背景扩展了答案。您从我最初的解释中清楚地理解了它,但希望这对未来的更多人有用。
  • 这是一个非常好的答案,我什至没有在寻找 Sinatra 信息。希望你的努力得到更多的支持:-)
  • 这是一个绝妙的答案!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-13
  • 2023-03-26
相关资源
最近更新 更多