affectation de accesseur ajouté Dynamiquement ne fonctionne pas lors de l'appel via le bloc instance_eval en Ruby
-
25-09-2019 - |
Question
J'ai une classe à laquelle j'ajouter attribuer dynamiquement à l'exécution accesseurs. Cette classe fait partie d'un DSL, dans lequel les blocs sont transmis à des méthodes de configuration et appelées à l'aide instance_eval. Cela permet dans le DSL de supprimer les références à « soi » lors du référencement des méthodes de la classe.
Cependant, je l'ai découvert que je peux faire référence aux attributs pour récupérer leurs valeurs, mais je suis incapable de les attribuer, à moins que le référencement explicity soi-même, comme l'exemple de code suivant illustre.
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?
La solution
La question arrises avec la façon dont Ruby traite par exemple et les variables 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 rubis.
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 bocks façon dont le 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 classe à la variable d'instance (comme cela est le cas avec l'appel à input_instance dans mon par exemple).
Il existe trois façons d'obtenir le comportement que vous voulez.
Utilisez des 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".