Ruby 元编程:define_method 块不维护范围

Ruby metaprogramming: define_method block not maintaining scope

我正在努力动态修补一堆 classes 和方法(大多数时候这些方法并不简单 "puts" 就像我能找到的很多例子一样在互联网上)

例如我有以下代码: foo.rb

module Base
  class Foo
    def info
      puts 'Foo#info called'
    end
  end 
 end

&我还有以下class: test.rb

module Base
  class Test
    def print
      puts "Test#print called"
      Foo.new.info
    end
  end
end

然后在 main.rb 中,我想在同一模块中添加一个使用 class 的方法(在本例中为 Foo)

require_relative './foo'
require_relative './test'


new_method_content = "puts 'hi'
                      Foo.new.info"

Base::Test.instance_eval do
  def asdf
    puts "Test#asdf called"
    Foo.new.info
   end
end

其中,执行时将获得以下内容:

Uncaught exception: uninitialized constant Foo

哪种对我有意义,因为 main.rb 文件不知道我想要 Base::Foo,但是,我需要一种方法来维护查找范围,因为 Base::Test 应该能够找到我想要的 class Foo。

Base::Test.instance_eval do
  def asdf
    puts "Test#asdf called"
    Foo.new.info
   end
end

我已经进行了相当多的谷歌搜索和 SO'ing,但没有找到任何关于如何在 class_eval/instance_eval/module_eval/define_method 期间保持恒定查找范围的信息(我已经尝试了很多 Ruby各种黑魔法都以不同程度的失败告终 lol)

https://cirw.in/blog/constant-lookup

Confusingly however, if you pass a String to these methods, then the String is evaluated with Module.nesting containing just the class itself (for class_eval) or just the singleton class of the object (for instance_eval).

& 还有这个: https://bugs.ruby-lang.org/issues/6838

Evaluates the string or block in the context of mod, except that when a block is given, constant/class variable lookup is not affected.

所以我的问题是: 如何重新定义方法但保持 constant/class 范围?

我一直在尝试其他一些事情(在 main.rb 的背景下):

Base::Test.class_eval('def asdf; puts "Test#asdf called"; Foo.new.info; end')
Base::Test.new.asdf
=>
Test#asdf called
Uncaught exception: uninitialized constant Base::Test::Foo
Did you mean?  Base::Foo

(这是一个差异问题,因为它试图从评估的模块嵌套中查找它?我不确定为什么它不尝试 Base::Test 中可用的所有模块路径,但我会认为它会尝试不存在的 Base::Test::Foo ,因此它会在模块树上寻找存在的 class(Base::Foo))

当您像这样引用 class Base::Test 时,ruby 不会将 Base:: 作为查找常量的模块上下文。这是正常行为,如果您直接定义方法,也不会起作用。

但是你可以这样做:

module Base
  Test.instance_eval do
    def asdf
      puts "Test#asdf called"
      Foo.new.info
    end
  end
end