ruby:如何正确地要求(避免循环依赖)

Posted

技术标签:

【中文标题】ruby:如何正确地要求(避免循环依赖)【英文标题】:ruby: how to require correctly (to avoid circular dependencies) 【发布时间】:2011-12-24 20:24:55 【问题描述】:

今天我遇到了一个奇怪的问题: 在模块上出现“缺少方法”错误,但该方法在那里并且需要定义该模块的文件。经过一番搜索,我发现了一个循环依赖项,其中 2 个文件相互需要,现在我假设 ruby​​ 默默地中止循环需要。


编辑开始:示例

文件'a.rb':

require './b.rb'

module A
    def self.do_something
        puts 'doing..'
    end
end

文件'b.rb':

require './a.rb'

module B
    def self.calling
        ::A.do_something
    end
end

B.calling

执行 b.rb 会得到b.rb:5:in 'calling': uninitialized constant A (NameError)。这两个文件都必须存在要求,因为它们旨在从命令行自行运行(我省略了该代码以使其简短)。 所以 B.calling 必须在那里。一种可能的解决方案是将需求包装在 if __FILE__ == $0 中,但这似乎不是正确的方法。

编辑结束


为了避免这些难以发现的错误(顺便说一句,如果 require 抛出异常不是更好吗?),是否有一些关于如何构建项目以及在哪里需要什么的指南/规则?例如,如果我有

module MainModule
  module SubModule
    module SubSubModule
    end
  end
end

我应该在哪里需要子模块?全部在 main 中,还是只有在 main 中的 sub 和 sub 中的 subsub?

任何帮助都会非常好。

总结

forforfs answer 和 cmets 中讨论了为什么会发生这种情况。

到目前为止,最佳实践(正如lain指出或暗示的那样)似乎如下(如果我错了,请纠正我):

    将顶层命名空间中的每个模块或类放入以模块/类命名的文件中。在我的示例中,这将是 1 个名为“main_module.rb”的文件。 如果有子模块或子类,则创建一个以模块/类命名的目录(在我的示例中为目录'main_module',并将子类/子模块的文件放在那里(在示例1中名为'sub_module.rb'的文件中) . 对命名空间的每个级别重复此操作。 需要逐步说明(在示例中,MainModule 需要SubModuleSubmodule 需要SubSubModule) 将“运行”代码与“定义”代码分开。在运行代码中需要一次您的***模块/类,因此由于 2. 您现在应该可以使用所有库功能,并且您可以运行任何已定义的方法。

感谢所有回答/评论的人,对我帮助很大!

【问题讨论】:

您的示例不会产生 NameError;这个对我有用。看起来您已经将代码过度简化到无法证明您的问题的程度。我对您有关加载顺序的问题的回答将是您的示例代码! :-) 这很奇怪,我实际上运行了代码,它确实产生了那个错误消息。我用的是 ruby​​ 1.9.2p180,你用的是什么? 好吧,我用 ruby​​ 1.8.7p330 试过了,这也给了我同样的错误。也许你没有正确复制/粘贴。 【参考方案1】:

不久前在 Ruby 邮件列表上询问了这个问题后,当我以前在我的库中有一个文件只是为了需要一些东西时,我改为以下两条规则:

    如果一个文件需要同一个库中另一个文件的代码,我在需要代码的文件中使用require_relative

    如果文件需要来自不同库的代码,我在需要代码的文件中使用require

据我了解,Ruby 按照要求的顺序要求,因此循环依赖无关紧要。

(Ruby v1.9.2)

回答关于显示循环依赖问题的示例的评论:

实际上,该示例的问题不在于要求是循环的,而是在要求完成之前调用了B.calling。如果您从 b.rb 中删除 B.calling ,它工作正常。例如,在 irb 中没有 B.calling 在代码文件中但之后运行:

$ irb 需要'/Volumes/RubyProjects/Test/***8057625/b.rb' => 真的 B.调用 正在做.. => 无

【讨论】:

此外,请注意(与load 不同)多个requires 到同一个文件只会导致文件的单次加载。 感谢您的回答。但是您可以尝试使用我添加的示例,循环需求存在问题。 是的,这行得通,但这些文件应该可以自己运行,所以B.calling 必须在那里(见编辑)。或者这只是一个非常糟糕的主意,只会造成麻烦? 正如@sheldonh 上面所说的,现在代码过于简单,如果我不得不评论它,我只会说不要那样写。如果两个文件都需要单独运行但共享行为,那么我建议采用该共享行为并将其放入两者都需要的第三个文件中。我还倾向于将“库”类型代码(不能自行运行的东西)与“可运行”类型代码(可以运行的东西)分开,因此 B.calling 将位于其自己的文件中。 好的,谢谢,我认为这是个好建议。从现在开始,我会将使用库的脚本与库本身分开。你对我问题的最后一部分有什么建议吗?【参考方案2】:

您希望已经知道的一些基本知识:

    Ruby 是解释的,而不是编译的,所以你不能执行解释器没有看到的任何代码。

    require 只是将文件中的代码插入程序的该点,换句话说,程序顶部的 require 将在底部的 require 之前解释。

(注意:已编辑以说明 require 语句的行为) 因此,如果您要这样做: ruby a.rb 这是 ruby​​ 解释器会看到并执行的:

#load file b.rb <- from require './b.rb' in 'a.rb' file

#load file a.rb <- from require './a.rb' in 'b.rb' file
  #this runs because a.rb has not yet been required

#second attempt to load b.rb but is ignored <- from require './b.rb' in 'a.rb' file

#finish loading the rest of a.rb

module A
  def self.do_something
    puts 'doing..'
  end
end

#finish loading the rest of b.rb

module B
  def self.calling
    ::A.do_something
  end
end
B.calling

#Works because everything is defined 

如果你先运行 b,ruby b.rb,解释器会看到:

#load file a.rb <- from require './a.rb' in 'b.rb' file

#load file b.rb <- from require './b.rb' in 'a.rb' file
  #this runs because b.rb has not yet been required

#second attempt to load a.rb but is ignored <- from require './a.rb' in 'b.rb' file

#finish loading the rest of b.rb
module B
  def self.calling
    ::A.do_something
  end
end
B.calling #NameError, ::A.do_something hasn't been defined yet.

希望这能解释其他人给你的好答案,如果你仔细想想,为什么很难回答你最后一个关于在哪里放置 require 语句的问题。使用 Ruby,您需要 文件 而不是模块,因此在代码中放置 require 的位置取决于文件的组织方式。

如果您绝对需要能够定义模块并以随机顺序执行方法,那么您可以实现类似的东西来收集对尚不存在的模块的调用,然后在它们出现时调用它们。

module Delay
  @@q =   
  def self.call_mod(*args) #args format is method_name, mod_name, *args
    mod_name = args.shift
    method_name = args.shift
    #remaining args are still in args
    mod = Object.const_get(mod_name.to_sym)
    mod.send(method_name.to_sym, *args)
  end

  def self.exec(mod_name, *args)
    begin
      args.unshift(mod_name)
      self.call_mod(*args)
    rescue NameError, NoMethodError
      @@q[mod_name] ||= []
      @@q[mod_name] << args
    end
  end

  def self.included(mod)
    #get queued methods
    q_list = @@q[mod.name.to_sym]
    return unless q_list
    #execute delayed methods now that module exists
    q_list.each do |args|
      self.call_mod(*args)
    end
  end
end 

请务必先定义延迟模块,然后再使用Delay.exec(:B, :calling, any_other_args),而不是调用B.calling。所以如果你在延迟模块之后有这个:

Delay.exec(:B, :calling)   #Module B is not defined

module B
  def self.calling
    ::A.do_something
  end
  include Delay #must be *after* any callable method defs
end

module A
  def self.do_something
    puts 'doing..'
  end
  include Delay #must be *after* any callable method defs
end

结果:

#=> doing..

最后一步是将代码分解为文件。一种方法是拥有三个文件

delay.rb   #holds just Delay module
a.rb       #holds the A module and any calls to other modules 
b.rb       #holds the B module and any calls to other modules

只要您确保 require 'delay' 是模块文件(a.rb 和 b.rb)的第一行,并且模块末尾包含延迟,那么一切都会正常。

最后说明:只有当您无法将定义代码与模块执行调用分离时,此实现才有意义。

【讨论】:

感谢您的解释。我总是对圆形要求中真正发生的事情感到有些困惑。目前在我看来是这样的:在运行 b.rb 时,解析器遇到 require 'a.rb 并开始解析 a.rb。它在那里遇到require 'a.rb,并且由于尚未需要b.rb,因此开始解析b.rb。还有一个require 'a.rb,但是已经需要a.rb,所以它在b.rb 中继续。最后它执行B.calling,它试图执行::A.do_something。这失败了,因为 a.rb 的解析尚未完成(或已完全中止?) 几乎,我认为您缺少的是 require 只加载文件一次。所以它是这样的: (哦,还要记住,在 Ruby 中运行和解析本质上是一回事)。所以在运行 b.rb 时,解析器遇到require 'a.rb' 并开始解析 a.rb。首先遇到的是require 'b.rb',但是b.rb已经在解析器中了,所以require 'b.rb'被忽略了。 a.rb 的其余部分定义了模块 A,然后我们转到 b.rb 的其余部分,它定义了模块 B,然后运行 ​​B.calling(有效)。但是,如果你从 a.rb 而不是 b.rb 开始,事情就行不通了。 运行 b.rb 不起作用! (它会产生一个 NameError。但是,运行 a.rb 'works',它会产生 'doing..')。起初这让我感到困惑。我认为运行与要求不同(在名为 c.rb 的文件中尝试 puts 'c'; require './c.rb'。如果你运行它,你会得到两次'c'!),这是造成这个问题的主要原因。 抱歉,是的,require 行为不同。我编辑了原始答案以更准确地反映这一点。

以上是关于ruby:如何正确地要求(避免循环依赖)的主要内容,如果未能解决你的问题,请参考以下文章

在 Nestjs/GraphQL 中使用接口时避免循环依赖

如何避免“React Hook useEffect 缺少依赖项”和无限循环

CMake 为 VS 项目生成循环依赖,但不生成文件。如何避免?

使用依赖于存储桶名称的模板设置 S3 存储桶策略时如何避免循环错误?

如何正确处理 Python 中的循环模块依赖关系?

spring的循环依赖是什么,设计的时候怎么避免,而spring又是怎么解决的?