2012-05-15 16 views
5

He intentado configurar un sistema mediante el cual puedo generar una serie de clases de Ruby similares, distinguidas por un parámetro entero, que guardo en una variable de clase de la clase relevante, algo parecido a las plantillas de C++.Clases definidas dinámicamente que comparten datos de forma incorrecta: error o error de codificación.

Sin embargo, la referencia (y, por lo tanto, la creación) de una nueva versión de la clase con plantilla sobrescribe los parámetros guardados en las versiones anteriores, y no sé por qué.

He aquí un ejemplo mínimo

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> 

¿He encontrado un fallo en mi Ruby (Ruby 1.9.2p290 (2011-07-09) [i386-mingw32]), o tiene simplemente codificado algo mal?

+0

Bueno, dediqué 45 minutos tratando de resolver esto, y supuse que algo andaba mal con las encuadernaciones variables porque estás ejecutando un bloque dentro de la clase 'Object'. Sin embargo, no me puedo atribuir la respuesta porque finalmente encontré la explicación como se sospecha aquí: http://stackoverflow.com/questions/10109925/ruby-unexpected-results-from-class-exec-when-defining-class- variable . Su solución es convertir ese bloque en una cadena y evaluarlo para que se compile en el contexto correcto. – Casper

+0

@Casper cuando la respuesta es "usar evaluación de cadena en lugar de evaluación de bloque", generalmente es hora de buscar una mejor respuesta. La evaluación de cadenas es peligrosa y frágil, a veces es lo que necesita, pero estimo que más del 95% del tiempo lo veo utilizado. Hay una forma menos peligrosa disponible. – dbenhur

Respuesta

1

porque primero sintácticamente referencia @@base en el contexto de la clase Object, que es una variable de clase de objeto y todas las subclases de TemplX objeto hacen referencia a la clase var de la superclase. Puede cambiar su código para usar Module#class_variable_set y class_variable_get para evitar el enlace en la superclase.

Algunos otros problemas con su código: Noto que no hizo make_templ un par de método de clase de self.const_missing, aunque se envió correctamente porque Object es un antecesor de Class. Lo mejor es evitar todas las formas de eval (cadena) cuando existen otros métodos. No deberías subir NameError si no manejas el const_missing, sino más bien enviarlo a super como alguien más puede estar en la cadena y quieres hacer algo para resolver la constante.

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 

Las variables de clase tienen propiedades de mezcla de información interesantes ya menudo indeseables a través de la herencia. Has golpeado a uno de los gotchas. No sé qué otras propiedades necesita alrededor de @@base, pero parece que obtendrá un mejor aislamiento y resultados menos sorprendentes utilizando una variable de instancia de clase en su lugar. Para más explicaciones: Fowler, RailsTips

+0

¡Si pudiera +1 más de una vez lo haría! Creo que usaré variables de instancia de clase, pero tus otros consejos también son dorados.Pensé que 'super' en' const_missing' funcionaría, ya que 'Object' no define' const_missing'; y yo no podría averiguar cómo crear el nombre de clase dinámico sin 'eval', así que es genial. ¡Aclamaciones! – Chowlett

1

El comentario de @Casper ayuda a señalar por qué su código no funciona. Para una solución, considere usar variables de instancia de clase en lugar de variables de clase. Esto debería ayudar a evitar tener que eval y esquivar las trampas comunes de la utilización de variables de clase:


EDIT: añade refactorización de @dbenhur, el cambio de variable de clase a instancia de clase variable.

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 
+0

+1 por sugerir vars de instancia de clase, -1 por no abordar los otros problemas con código de OP (innecesaria eval de cadena, no super en la ruta else de const_missing) – dbenhur

Cuestiones relacionadas