您观察到的是 rake 文件命名空间中的方法重新定义了以前定义的具有相同名称的方法。这样做的原因是 Rake namespaces 与 Ruby 命名空间(类或模块)非常不同,实际上它们仅作为 其中定义的任务名称的命名空间,但没有别的。因此,如果将任务 a 放置在 a 命名空间中,则任务 a:a 将变为任务 a:a,但任务之外的其他代码共享 same 全局命名空间。
这一事实,连同 Rake 在运行给定任务之前加载所有任务 的事实,解释了为什么要重新定义该方法。
TL;DR:名称冲突的解决方案/提示
您不能期望将两个具有相同名称(或任何其他代码)的方法放在单独的 namespaces 中,但外部任务会正常工作。不过,这里有一些提示可以解决这种情况:
将方法放在任务中。如果在a:a 和b:b 任务中定义了两个msg 方法,那么这两个rake 任务都将正常运行并显示预期的消息。
-
如果您需要在多个 rake 任务中使用来自 rake 的 namespace 的代码,将方法/代码提取到真正的 Ruby 命名空间,例如两个模块,include 的代码在需要它的任务中。考虑对样本耙的这种重写:
# lib/tasks/a.rake:
module HelperMethodsA
def msg(msg)
puts "MSG SENT FROM Task A: #{msg}"
end
end
namespace :a do
desc "Task A"
task(:a => :environment) do
include HelperMethodsA
msg('I AM TASK A')
end
end
# lib/tasks/b.rake:
module HelperMethodsB
def msg(msg)
puts "MSG SENT FROM Task B: #{msg}"
end
end
namespace :b do
desc "Task B"
task(:b => :environment) do
include HelperMethodsB
msg('I AM TASK B')
end
end
因为这两个模块有不同的名称,并且因为它们在各自的任务中是included,所以两个 rake 任务将再次按预期运行。
现在,让我们借助源代码来证明上述说法...
证明 Rake 首先加载所有任务以及这样做的原因
这个很简单。在主要的Rakefile 中,您总能找到以下行:
Rails.application.load_tasks
该方法最终从 Rails 引擎调用以下代码:
def run_tasks_blocks(*) #:nodoc:
super
paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
因此,它会在 lib/tasks 目录中搜索所有 rake 文件,并按排序顺序依次加载它们。这就是为什么 b.rake 文件将在 a.rake 之后加载,并且其中的任何内容都可能重新定义 a.rake 和所有以前加载的 rake 文件的代码。
Rake 必须加载所有 rake 文件,因为 rake namespace 名称不必与 rake 文件名相同,因此无法从任务/命名空间名称推断 rake 文件名。
证明 rake 的 namespaces 不构成真正的类 Ruby 命名空间
加载 rake 文件后,将执行 Rake DSL 语句以及 namespace 方法。该方法获取其中定义的代码块并执行它(使用yield)在Rake.application 对象的上下文中,这是Rake::Application 类的一个单例对象,在它们之间共享所有 rake 任务。没有为命名空间创建动态模块/类,它只是在主对象上下文中执行。
# this reuses the shared Rake.application object and executes the namespace code inside it
def namespace(name=nil, &block)
# ...
Rake.application.in_namespace(name, &block)
end
# the namespace block is yielded in the context of Rake.application shared object
def in_namespace(name)
# ...
@scope = Scope.new(name, @scope)
ns = NameSpace.new(self, @scope)
yield(ns)
# ...
end
查看相关来源here和here。
Rake 任务确实构成了 ruby 命名空间
不过,对于 Rake 任务本身,情况有所不同。对于每个任务,将创建Rake::Task 类(或类似类)的一个单独的对象,并且任务代码在该对象的上下文中运行。对象的创建在任务管理器中的intern method中完成:
def intern(task_class, task_name)
@tasks[task_name.to_s] ||= task_class.new(task_name, self)
end
Rake 的作者的一句话
最后,这一切都得到了interesting discussion on github 的证实,该interesting discussion on github 处理了一个非常相似和相关的问题,我们可以引用 Rake 的原作者 Jim Weirich 的话:
由于命名空间不引入真正的方法作用域,作用域唯一真正的可能性是 DSL 模块。
...
也许,有一天,Rake 命名空间将成为完整的类范围实体,并有一个地方可以挂起惰性 let 定义,但我们还没有。