3

根据这个答案,至少在 IRB 中,可以在顶层使用一个常量进入全局命名空间。include天真地,我以为我可以在模块中做同样的把戏:

module Foo
  class Bar
    def frob(val)
      "frobbed #{val}"
    end
  end
end

module Baz
  include Foo
  class Qux
    def initialize(val)
      @val = val
      @bar = Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

但是,尝试使用它会产生:

NameError: uninitialized constant Baz::Qux::Bar

然后我尝试覆盖Baz::const_missing转发到Foo,但它没有被调用。Baz::Qux::const_missing确实如此,但我想那个时候解释器已经放弃了查看Baz.

我怎样才能将Foo常量导入到Baz这样的类中,Baz而不必限定它们?

(请注意,出于目录结构和模块名称之间的一致性的原因,我不只是想声明Quxetc. inFoo而不是。)Baz


ETA:如果我创建另一个类Baz::CorgeBaz::Qux可以无条件地引用它。很明显,存在某种“对等范围”的感觉。我知道有几种方法可以使Qux访问成为可能Bar,但我正在寻找一种方法,让所有类在没有资格的情况下Baz访问Bar(以及所有其他类)。Foo

4

4 回答 4

0

Here's what I eventually came up with. In the same file that initially declares Baz:

module Foo
  def self.included(base)
    constants.each { |c| base.const_set(c, const_get("#{self}::#{c}")) }
  end
end

module Baz
  include Foo
  #... etc.
end

When Baz includes Foo, it will set a corresponding constant Baz::Whatever for every Foo::Whatever, with Foo::Whatever as the value.

If you're worried Foo may already define self.included, you can use alias_method to adjust for that:

module Foo
  alias_method :old_included, :included if self.method_defined? :included
  def self.included(base)
    old_included(base) if method_defined? :old_included
    constants.each { |c| base.const_set(c, const_get("#{self}::#{c}")) }
  end
end

This approach has two limitations --

  1. All the constants (including classes) in Foo that we care about must be defined at the time include Foo is evaluated -- extensions added to Foo later will not be captured.
  2. The file that defines Foo.included here must be required before any file in Baz that uses any of those constants -- simple enough if clients are just using require 'baz' to pull in a baz.rb that in turn uses Dir.glob or similar to load all the other Baz files, but it's important not to require those files directly.

Roko's answer gets around problem (1) above using const_missing, but it still has an analogous problem to (2), in that one has to ensure add_const_missing_to_classes is called after all classes in Baz are defined. It's a shame there's no const_added hook.

I suspect the const_missing approach also suffers performance-wise by depending on const_missing and const_get for every constant reference. This might be mitigated by a hybrid that caches the results, i.e. by calling const_set in const_missing, but I haven't explored that since trying to figure out scoping inside define_singleton_method always gives me a headache.

于 2015-10-06T22:48:03.923 回答
0

好的..问题是常量(出于某种原因)没有Quz classthe module Baz scope. 如果我们能够以某种方式将(当前单词?)Quz 中的常量调用委托给上层(Baz)范围 - 我们可以:

使用模块#const_missing

我将展示两次:一次用于您的私人案例,一次用于更一般的方法。

1)解决私人案例:

module Foo
  class Bar
    def frob(val)
      "frobbed #{val}"
    end
  end
end

module Baz
  include Foo
  class Qux
    def self.const_missing(c)
      #const_get("#{self.to_s.split('::')[-2]}::#{c}")
      const_get("Baz::#{c}")
    end

    def initialize(val)
      @val = val
      @bar = Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

puts Baz::Qux.new('My Value').twiddle

那么这里会发生什么?几乎相同的事情 - 只是当收到错误时 - 它有点被拯救并到达(或回退到)const_missing function接收新值 - 这适用于常量和类(显然它们是相同的类型)。

但这意味着我们必须将该self.const_missing(c)方法添加到模块 Baz 中的每个类中——或者我们可以迭代模块 Baz 中的每个类并添加它(可能有更好的方法来做到这一点,但它确实有效)


2)更自动化的方法:

module Foo
  class Bar
    def frob(val)
      "frobbed #{val}"
    end
  end
end

module Baz

  class Qux
    def initialize(val)
      @val = val
      @bar = Bar.new
    end

    def twiddle
      @bar.frob(@val)
    end
  end

  def self.add_const_missing_to_classes
    module_name = self.to_s

    #note 1
    classes_arr = constants.select{|x| const_get(x).instance_of? Class} #since constants get also constants we only take classes

    #iterate classes inside module Baz and for each adding the const_missing method
    classes_arr.each do |klass| #for example klass is Qux
      const_get(klass).define_singleton_method(:const_missing) do |c|
        const_get("#{module_name}::#{c}")
      end
    end
  end

  add_const_missing_to_classes

  #we moved the include Foo to the end so that (see note1) constants doesn't return Foo's classes as well
  include Foo
end

puts Baz::Qux.new('My Value').twiddle

在定义了所有类之后,在模块 Baz 的末尾。我们迭代它们(在add_const_missing_to_classes方法内部)。为了选择它们,我们使用Module#constants方法,它返回一个模块常量数组——意味着 CONSTANTS 和 CLASSES,所以我们使用 select 方法只对类起作用。

然后我们迭代找到的类并将const_missing类方法添加到类中。

请注意,我们将include Foo方法移到了末尾——因为我们希望该constants方法不包含来自模块 Foo 的常量。

Surly有更好的方法来做到这一点。但我相信OP的问题:

如何将 Foo 常量导入 Baz 以便 Baz 下的类不必限定它们?

已回答

于 2015-10-06T21:48:51.940 回答
-1

从给出的错误中可以看出:NameError: uninitialized constant Baz::Qux::Bar

它试图找到Bar class内部Qux class范围 - 但在那里找不到 - 为什么会这样?

因为它不在这个Baz modlue范围内——它在你使用的范围内include Foo

所以你有两个选择:1)在调用时解决正确的范围Bar class,所以改变这个:

@bar = Bar.new

进入这个:

@bar = Baz::Bar.new

像那样:

module Baz
  include Foo
  class Qux
    def initialize(val)
      @val = val
      @bar = Baz::Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

或者,

2)将 插入include Foo自身class Qux

module Baz
  class Qux
    include Foo
    def initialize(val)
      @val = val
      @bar = Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

- 编辑 -

正如 joanbm 所说,这并不能解释这种行为。您可能想看看Ruby Modules 中的 Scope of Constants。尽管那篇文章是关于常量(不是类)的,但原理是一样的。

于 2015-10-06T00:27:18.710 回答
-1

由于 Foo 包含在 Baz 中,因此可以找到 Bar —— Qux 中找不到它。所以你可以改变

@bar = Bar.new

@bar = Baz::Bar.new

或在Qux 类中移动include Foo

于 2015-10-07T03:41:32.567 回答