2

一般的なメソッドをモジュールまたはクラスに移動し、別のモジュールで名前空間が設定されている新しいクラスに含める/継承することで、ドライにしようとしています。同じモジュールの下に 2 つのクラスの名前空間がある場合、同じ名前空間の下にある限り、モジュール名を含めずにそれらを呼び出すことができます。しかし、名前空間スコープの変更とは異なるモジュールからメソッドが含まれている場合、それを回避する理由や方法がわかりません。

例えば。このコードは機能し、「バー」を返します。

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: uninitialized constant 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 がブロックではなく文字列を評価している場合、スコープは Foo ではなく Foo::Bar になります。

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 ヘルパーの定数化を使用していることに注意してください)。

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
4

3 に答える 3

1

この問題を理解するには、Ruby で定数ルックアップがどのように機能するかを理解する必要があります。メソッド検索とは異なります。このコードでは:

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

Helperで定義されている「ヘルパー」という定数ではなくA、 、Mixins、または最上位レベル (つまりObject) で定義されている「ヘルパー」という定数を指します。このコードを classでコーディングしたからといって、それは変わりません。「レキシカル バインディング」と「ダイナミック バインディング」の違いがわかれば、Ruby の定数解決はレキシカルバインディングを使用していると言えます。動的バインディングを使用することを期待しています。Barclass_evalBar

渡したブロックは1 回base.class_evalバイトコードにコンパイルされ、その後フックが呼び出されるたびに、同じプリコンパイル済みブロック ( への参照を含む) が別のクラス ( )で実行されることに注意してください。インタプリタは、実行するたびにブロックを新たに解析およびコンパイルしません。includedHelperbaseselfbase.class_eval

一方、Stringをに渡すと、フックが実行されるたびclass_evalにその文字列が解析され、新たにコンパイルされます。重要: String から編集されたコードは、 null 字句環境で評価されます。つまり、周囲のメソッドのローカル変数は、文字列から編集されているコードでは使用できません。さらに重要なのは、周囲のスコープがed コード内からの定数ルックアップに影響を与えないことも意味します。includedevalevaleval

定数参照を動的に解決したい場合、明示的な定数参照は機能しません。これは、言語が意図したとおりに動作する方法ではありません (正当な理由があります)。考えてみてください: のクラスに応じて、定数参照が動的に解決された場合、 のようなものへの参照が実行時にどのように解決されるかselfを予測することはできませんでした。モジュールにこのようなコードがある場合...ArrayHash

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

...そして、モジュールがネストされたクラスを持つクラスに混在していたため、標準ライブラリからではなく、ネストされたクラスHashHash.[]参照していました! 明らかに、定数参照を動的に解決すると、名前の衝突や関連するバグが発生する可能性が非常に高くなります。Hash

メソッドルックアップでは、それは別のことです。OOP (少なくとも OOP の Ruby フレーバー) の全体的な概念は、メソッド呼び出し (つまりメッセージ) が何をするかは受信側のクラスに依存するということです。

レシーバーのクラスに応じて動的に定数を見つけたい場合は、 を使用して行うことができますself.class.const_getevalこれは、同じ効果を得るために String を使用するよりも間違いなくクリーンです。

于 2012-05-09T15:01:53.823 に答える
0

Ruby の定数ルックアップは、過去数回のメジャー リリースで #class_eval に関して変更されました。詳細については、この投稿を参照してください: http://jfire.posterous.com/constant-lookup-in-ruby

于 2012-05-09T16:22:33.990 に答える
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

Ruby で定数が解決される方法のため、Helper クラスは Foo::Bar の下に名前空間を設定する必要があることに注意してください。

于 2012-05-08T23:09:40.393 に答える