动态定义的类错误地共享数据 - 错误或编码错误?

时间:2012-05-15 15:35:18

标签: ruby metaprogramming

我一直试图建立一个系统,我可以生成一系列类似的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]),还是我编写了错误的错误?

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:1)

因为你首先在Object类的上下文中语法引用@@base,所以它是Object的一个类变量,而object的所有TemplX子类都引用了超类的类var。您可以更改代码以使用Module#class_variable_setclass_variable_get来避免超类中的绑定。

您的代码的一些其他问题:我注意到您没有使make_templ成为self.const_missing的类方法对等,尽管它成功调度,因为Object是Class的祖先。当存在其他方法时,最好避免使用所有形式的eval(字符串)。如果你不处理const_missing,你不应该引发NameError,而是调度到super,因为其他人可能在链中,并且想要做一些事来解决常量。

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

类变量通过继承具有有趣且通常不合需要的信息混合属性。你遇到了其中一个陷阱。我不知道你需要在@@base周围使用什么其他属性,但看起来你可能会使用类实例变量来获得更好的隔离和更少惊人的结果。有关详细说明,请执行以下操作:FowlerRailsTips