5

私は、関連するクラスのクラス変数に保存する、整数パラメーターによって区別される一連の同様の Ruby クラスを生成できるシステムをセットアップしようとしています - C++ テンプレートに似ています。

ただし、テンプレート化されたクラスの新しいバージョンを参照 (したがって、作成) すると、以前のバージョンで保存されたパラメーターが上書きされます。その理由はわかりません。

これは最小限の例です

class Object
  def self.const_missing(name)
    if name =~ /^Templ(\d+)$/
      return make_templ $1.to_i
    else
      raise NameError.new("uninitialised constant #{name}")
    end
  end

private
  def make_templ(base)
    # Make sure we don't define twice
    if Object.const_defined? "Templ#{base}"
      return Object.const_get "Templ#{base}"
    else
      # Define a stub class
      Object.class_eval "class Templ#{base}; end"

      # Open the class and define the actual things we need.
      Object.const_get("Templ#{base}").class_exec(base) do |in_base|        
        @@base = in_base

        def initialize
          puts "Inited with base == #{@@base}"
        end
      end

      Object.const_get("Templ#{base}")
    end
  end
end

irb(main):002:0> Templ1.new
Inited with base == 1
=> #<Templ1:0x26c11c8>
irb(main):003:0> Templ2.new
Inited with base == 2
=> #<Templ2:0x20a8370>
irb(main):004:0> Templ1.new
Inited with base == 2
=> #<Templ1:0x261d908>

私の Ruby (ruby 1.9.2p290 (2011-07-09) [i386-mingw32]) でバグを見つけたのでしょうか、それとも単純に何か間違ったコードを書いたのでしょうか?

4

2 に答える 2

1

@Casperからのコメントは、コードが機能しない理由を指摘するのに役立ちます。修正するには、クラス変数の代わりにクラスインスタンス変数を使用することを検討してください。evalこれは、クラス変数を使用する際の一般的な落とし穴を回避し、回避するのに役立ちます。


編集: @dbenhurからのリファクタリングを追加し、クラス変数をクラスインスタンス変数に切り替えました。

class Object
  def self.const_missing(name)
    name =~ /^Templ(\d+)$/ ? make_templ($1.to_i) : super
  end

private
  def self.make_templ(base)
    klass_name = "Templ#{base}"
    if const_defined? klass_name
      const_get klass_name
    else
      klass = Class.new(Object) do
        class << self
          attr_accessor :base
        end
        self.base = base
        def initialize
          puts "Inited with base == #{self.class.base}"
        end
      end
      const_set klass_name, klass    
    end
  end
end

puts Templ1.new.class.base
# => Inited with base == 1
# => 1
puts Templ2.new.class.base
# => Inited with base == 2
# => 2
puts Templ1.new.class.base
# => Inited with base == 1
# => 1
于 2012-05-15T17:53:23.680 に答える
1

クラス Object のコンテキストで最初に構文的に参照@@baseするため、これは Object のクラス変数であり、object のすべての TemplX サブクラスはスーパークラスのクラス var を参照します。コードを変更してModule#class_variable_setを使用class_variable_getし、スーパークラスでのバインドを回避できます。

あなたのコードに関するいくつかの他の問題: Object が Class make_templ. self.const_missing他のメソッドが存在する場合は、すべての形式の eval(string) を避けるのが最善です。const_missing を処理しない場合は NameError を発生させるべきではありませんが、他の誰かがチェーン内にいて、定数を解決するために何かをしたい可能性があるため、スーパーにディスパッチする必要があります。

class Object
  def self.const_missing(name)
    if name =~ /^Templ(\d+)$/
      return make_templ $1.to_i
    end
    super
  end

private
  def self.make_templ(base)
    klass_name = "Templ#{base}"
    unless const_defined? klass_name
      klass = Class.new(Object) do
        class_variable_set :@@base, base
        def initialize
          puts "Inited with base == #{self.class.class_variable_get(:@@base)}"
        end
      end
      const_set klass_name, klass    
    end

    const_get klass_name
  end
end

クラス変数には、興味深い情報と、しばしば望ましくない情報が継承によって混合されています。あなたは落とし穴の 1 つをヒットしました。他にどのようなプロパティが必要かはわかりませんが@@base、代わりにクラス インスタンス変数を使用すると、分離が向上し、驚くべき結果が少なくなる可能性があります。詳細な説明: FowlerRailsTips

于 2012-05-15T18:04:17.683 に答える