2010-12-09 40 views
2

J'ai une classe à laquelle j'ajoute dynamiquement des accesseurs d'attribut au moment de l'exécution. Cette classe fait partie d'un DSL, par lequel les blocs sont passés aux méthodes de configuration et invoqués en utilisant instance_eval. Cela permet dans la DSL de supprimer les références à "self" lors du référencement des méthodes de la classe.L'affectation d'accesseurs ajoutée dynamiquement ne fonctionne pas lors de l'invocation d'un bloc via instance_eval dans Ruby

Cependant, j'ai découvert que je peux référencer les attributs pour récupérer leurs valeurs, mais je suis incapable de les assigner, sauf si je fais explicitement référence à self, comme l'illustre l'exemple de code suivant.

class Bar 

    def add_dynamic_attribute_to_class(name) 
    Bar.add_dynamic_attribute(name) 
    end 

    def invoke_block(&block) 
    instance_eval &block 
    end 

    def self.add_dynamic_attribute(name) 
    attr_accessor name 
    end 

end 

b = Bar.new 

b.add_dynamic_attribute_to_class 'dyn_attr' 

b.dyn_attr = 'Hello World!' 

# dyn_attr behaves like a local variable in this case 
b.invoke_block do 
    dyn_attr = 'Goodbye!' 
end 

# unchanged! 
puts "#{b.dyn_attr} but should be 'Goodbye!'" 

# works if explicitly reference self 
b.invoke_block do 
    self.dyn_attr = 'Goodbye!' 
end 

# changed... 
puts "#{b.dyn_attr} = 'Goodbye!" 

# using send works 
b.invoke_block do 
    send 'dyn_attr=', 'Hello Again' 
end 

# changed... 
puts "#{b.dyn_attr} = 'Hello Again!" 

# explain this... local variable or instance method? 
b.invoke_block do 

    puts "Retrieving... '#{dyn_attr}'" 

    # doesn't fail... but no effect 
    dyn_attr = 'Cheers' 

end 

# unchanged 
puts "#{b.dyn_attr} should be 'Cheers'" 

Quelqu'un peut-il expliquer pourquoi cela ne se comporte pas comme prévu?

Répondre

4

Le problème est lié à la manière dont Ruby traite les variables d'instance et locales. Ce qui se passe est que vous définissez une variable locale dans votre bloc instance_eval, plutôt que d'utiliser l'accesseur ruby.

Cela pourrait aider à expliquer:

class Foo 
    attr_accessor :bar 

    def input_local 
    bar = "local" 
    [bar, self.bar, @bar, bar()] 
    end 

    def input_instance 
    self.bar = "instance" 
    [bar, self.bar, @bar, bar()] 
    end 

    def input_both 
    bar = "local" 
    self.bar = "instance" 
    [bar, self.bar, @bar, bar()] 
    end 
end 

foo = Foo.new 
foo.input_local #["local", nil, nil, nil] 
foo.input_instance #["instance", "instance", "instance", "instance"] 
foo.input_both #["local", "instance", "instance", "instance"] 

La façon bocks travail est qu'ils établissent une distinction entre les variables locales et d'instance, mais si une variable locale ne se définit pas quand il est lecteur est appelé, les paramètres par défaut de la classe à la variable d'instance (comme c'est le cas avec l'appel à input_instance dans mon exemple).

Il existe trois façons d'obtenir le comportement souhaité.

Utiliser les variables d'instance:

 
    class Foo 
     attr_accessor :bar 

     def evaluate(&block) 
     instance_eval &block 
     end 
    end 

    foo = Foo.new 
    foo.evaluate do 
     @bar = "instance" 
    end 
    foo.bar #"instance" 

Utilisez une variable auto:

 
    class Foo 
     attr_accessor :bar 

     def evaluate(&block) 
     block.call(self) 
     end 
    end 

    foo = Foo.new 
    foo.evaluate do |c| 
     c.bar = "instance" 
    end 
    foo.bar #"instance" 

Utiliser les fonctions setter:

 
    class Foo 
     attr_reader :bar 
     def set_bar value 
     @bar = value 
     end 

     def evaluate(&block) 
     instance_eval &block 
     end 
    end 

    foo = Foo.new 
    foo.evaluate do 
     set_bar "instance" 
    end 
    foo.bar #"instance" 

Tous ces exemples mis foo.bar à "instance" .

+0

Je ne comprends pas complètement; vous dites que les classes ruby ​​font la distinction entre les variables locales et les variables d'instance, mais le problème est avec les méthodes accesseurs. c'est-à-dire bar! = @bar! = bar()! = bar =(). – VirtualStaticVoid

+0

BTW: J'ai réussi à contourner le problème en implémentant une méthode appelée "config", qui retourne self, sur la classe, afin que mon DSL soit mieux lu (ie au lieu d'utiliser self. * Maintenant config. *) – VirtualStaticVoid

+3

J'ai mis à jour le poste avec plus de détails. Le problème est que vous définissez sans le savoir une variable locale plutôt qu'une instance varaible. Pour être plus précis, bar == @bar == bar() == self.bar si une variable locale nommée bar n'est pas définie. Sinon bar! = @bar == bar() == self.bar si une variable locale nommée bar est définie. Pour être sûr, ne définissez pas les variables locales nommées "bar" ou n'utilisez que les autres méthodes. –