模块范围不对

时间:2012-05-08 22:49:29

标签: ruby-on-rails ruby

我试图将常用方法移动到模块或类中,并将其包含/继承在不同模块下命名空间的新类中。如果我在同一模块下有两个类名称空间,那么只要我在同一个名称空间下,我就可以在不包含模块名称的情况下调用它们。但是,如果我有一个方法包含在不同的模块而不是我的命名空间范围更改,我不知道为什么或如何避免它。

例如。此代码有效并返回'bar':

module Foo
  class Bar
    def test_it
      Helper.new.foo
    end
  end
end

module Foo
  class Helper
    def foo
      'bar'
    end
  end
end

Foo::Bar.new.test_it

但如果我将方法test_it移出到模块中,那么它就不再起作用了:NameError:未初始化的常量Mixins :: A :: Helper。

module Mixins; end

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        Helper.new.foo
      end
    end
  end
end

module Foo
  class Bar
    include Mixins::A
  end
end

module Foo
  class Helper
    def foo
      'bar'
    end
  end
end

Foo::Bar.new.test_it

此外,如果class_eval是evaling string而不是block,则scope变为Foo :: Bar而不是Foo。

module Mixins; end

module Mixins::A
  def self.included(base)
    base.class_eval %q{
      def test_it
        Helper.new.foo
      end
    }
  end
end

module Foo
  class Bar
    include Mixins::A
  end
end

module Foo
  class Helper
    def foo
      'bar'
    end
  end
end

Foo::Bar.new.test_it
谁有想法?

编辑:

感谢Wizard和Alex,我最终得到的代码不是很漂亮但完成了工作(注意它使用的是Rails helper constantize ):

module Mixins; end

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        _nesting::Helper
      end
      def _nesting
        @_nesting ||= self.class.name.split('::')[0..-2].join('::').constantize
      end
    end
  end
end

module Foo 
  class Helper
  end

  class Bar
    include Mixins::A
  end
end

module Foo2 
  class Helper
  end

  class Bar
    include Mixins::A
  end
end

Foo::Bar.new.test_it    #=> returns Foo::Helper
Foo2::Bar.new.test_it   #=> returns Foo2::Helper

3 个答案:

答案 0 :(得分:1)

要理解这个问题,您需要了解Ruby中常量查找的工作原理。它与方法查找不同。在这段代码中:

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        Helper.new.foo
      end
    end
  end
end

Helper指的是一个名为“助手”的常量,它位于AMixins或在顶级定义(即在Object中),< em> not 一个名为“Helper”的常量,它在Bar中定义。仅仅因为class_eval此类代码与Bar类相同,就不会改变它。如果您知道“词法绑定”和“动态绑定”之间的区别,那么您可以说Ruby中的常量分辨率使用 lexical 绑定。您期望它使用动态绑定。

请记住,传递给base.class_eval的块被编译为字节码一次,然后,每次调用included挂钩时,都会编译同一个预编译块(包括对Helper的引用)使用不同的类(base)作为self执行。每次执行base.class_eval时,解释器都不会重新解析和编译块。

另一方面,如果将 String 传递给class_eval,则每次 included挂钩都会重新解析并编译该字符串运行。重要信息:在 null词法环境中评估来自字符串eval的代码。这意味着来自周围方法的局部变量可用于来自字符串eval的代码。更重要的是,它还意味着周围的范围不会影响eval ed代码中的常量查找。

如果确实需要动态解析常量引用,则显式常量引用将永远不会起作用。这根本不是语言的工作方式(并且有充分的理由)。考虑一下:如果动态解析常量引用,取决于self的类,您永远无法预测在运行时如何解析对ArrayHash之类的引用的引用。如果你在模块中有这样的代码......

hash = Hash[array.map { |x| ... }]

...模块被混合到一个带有嵌套Hash类的类中,Hash.[]将引用嵌套类而不是Hash来自标准库!显然,动态解析常量引用对于名称冲突和相关错误的可能性太大了。

现在通过方法查找,这是另一回事。 OOP的整个概念(至少是OOP的Ruby风格)是方法调用(即消息)的作用取决于接收者的类。

如果你想要动态地找到一个常量,取决于接收器的类,你可以使用self.class.const_get来完成。这可以说比eval一个字符串更清晰,以达到同样的效果。

答案 1 :(得分:0)

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        Foo::Helper.new.foo

编辑:

在有机会玩了一下代码后,我发现了更多问题。我认为你不能完全按照自己的意思行事,但这很接近:

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        self.class.const_get(:Helper).new.foo
      end
    end
  end
end

module Foo
  class Bar
    include Mixins::A
  end
end

module Foo
  class Bar::Helper
    def foo
      'bar'
    end
  end
end

请注意,Helper类需要在Foo :: Bar下进行命名空间,因为方式常量在Ruby中得到解析。

答案 2 :(得分:0)

在过去的几个主要版本中,Ruby中的常量查找已经改变了#class_eval。有关详情,请参阅此帖子:http://jfire.posterous.com/constant-lookup-in-ruby