Dynamisch hinzugefügt Accessor Zuordnung funktioniert nicht, wenn der Block über instance_eval in Ruby Aufruf
-
25-09-2019 - |
Frage
Ich habe eine Klasse, auf die ich Attribut Accessoren dynamisch zur Laufzeit hinzufügen. Diese Klasse bildet einen Teil einer DSL, wobei Blöcke Konfigurationsmethoden übergeben bekommen und mit instance_eval aufgerufen. Dies macht es im DSL möglich Verweise auf ‚Selbst‘ zu entfernen, wenn Referenzierung Methoden der Klasse.
Allerdings habe ich entdeckt, dass ich die Attribute verweisen können ihre Werte zu erhalten, aber ich bin nicht in der Lage, sie zu vergeben, es sei denn, explicity selbstbeziehende, wie das folgende Codebeispiel veranschaulicht.
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'"
Kann mir jemand erklären, warum dies nicht verhält wie erwartet?
Lösung
Die Ausgabe arrises mit der Art und Weise, dass Ruby befasst sich mit Instanz und lokalen Variablen. Was geschieht, ist, dass Sie eine lokale Variable in Ihrem instance_eval Block setzen, anstatt den Rubin Zugriff verwendet wird.
Dies könnte helfen erklären:
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"]
Die Art und Weise bocks Arbeit ist, dass sie zwischen dem lokalen und Instanzvariablen unterscheiden, aber wenn eine lokale Variable nicht definiert ist, wenn es die Leser aufgerufen wird, die Klasse standardmäßig auf die Instanzvariable (wie es der Fall mit dem Aufruf von input_instance in meinem Beispiel).
Es gibt drei Möglichkeiten, um das gewünschte Verhalten zu erhalten.
Verwenden Instanzvariablen:
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"
Verwenden Sie eine Selbst Variable:
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"
Verwenden Sie set-Funktionen:
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"
Alle diese Beispiele setzen foo.bar auf "Instanz".