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
需要SubModule
,Submodule
需要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:如何正确地要求(避免循环依赖)的主要内容,如果未能解决你的问题,请参考以下文章
如何避免“React Hook useEffect 缺少依赖项”和无限循环
CMake 为 VS 项目生成循环依赖,但不生成文件。如何避免?