Comment puis-je ajouter une variable d'instance à une classe définie à durée, puis obtenir et définir sa valeur en dehors de la classe?
Je recherche une solution de métaprogrammation qui me permette de modifier l'instance de classe au moment de l'exécution au lieu de modifier le code source qui définissait à l'origine la classe. Quelques-unes des solutions expliquent comment déclarer des variables d'instance dans les définitions de classe, mais ce n'est pas ce que je demande.
Vous pouvez utiliser des accesseurs d'attribut:
class Array
attr_accessor :var
end
Vous pouvez désormais y accéder via:
array = []
array.var = 123
puts array.var
Notez que vous pouvez également utiliser attr_reader
ou attr_writer
pour définir uniquement les getters ou setters ou vous pouvez les définir manuellement comme tels:
class Array
attr_reader :getter_only_method
attr_writer :setter_only_method
# Manual definitions equivalent to using attr_reader/writer/accessor
def var
@var
end
def var=(value)
@var = value
end
end
Vous pouvez également utiliser des méthodes singleton si vous souhaitez simplement le définir sur une seule instance:
array = []
def array.var
@var
end
def array.var=(value)
@var = value
end
array.var = 123
puts array.var
Pour info, en réponse au commentaire sur cette réponse, la méthode singleton fonctionne très bien, et ce qui suit en est la preuve:
irb(main):001:0> class A
irb(main):002:1> attr_accessor :b
irb(main):003:1> end
=> nil
irb(main):004:0> a = A.new
=> #<A:0x7fbb4b0efe58>
irb(main):005:0> a.b = 1
=> 1
irb(main):006:0> a.b
=> 1
irb(main):007:0> def a.setit=(value)
irb(main):008:1> @b = value
irb(main):009:1> end
=> nil
irb(main):010:0> a.setit = 2
=> 2
irb(main):011:0> a.b
=> 2
irb(main):012:0>
Comme vous pouvez le voir, la méthode singleton setit
définira le même champ, @b
, comme celle définie à l'aide de l'attr_accessor ... donc une méthode singleton est une approche parfaitement valide pour cette question.
Ruby fournit des méthodes pour cela, instance_variable_get
et instance_variable_set
. ( documents )
Vous pouvez créer et affecter une nouvelle variable d'instance comme ceci:
>> foo = Object.new
=> #<Object:0x2aaaaaacc400>
>> foo.instance_variable_set(:@bar, "baz")
=> "baz"
>> foo.inspect
=> #<Object:0x2aaaaaacc400 @bar=\"baz\">
Si votre utilisation de "class MyObject" est une utilisation d'une classe ouverte, veuillez noter que vous redéfinissez la méthode d'initialisation.
Dans Ruby, il n'y a rien de tel que la surcharge ... seulement le remplacement ou la redéfinition ... en d'autres termes, il ne peut y avoir qu'une seule instance d'une méthode donnée, donc si vous la redéfinissez, elle est redéfinie ... et l'initialisation La méthode n'est pas différente (même si c'est ce que la nouvelle méthode des objets Class utilise).
Ainsi, ne redéfinissez jamais une méthode existante sans l'aliaser au préalable ... du moins si vous souhaitez accéder à la définition d'origine. Et redéfinir la méthode d'initialisation d'une classe inconnue peut être assez risqué.
En tout cas, je pense avoir une solution beaucoup plus simple pour vous, qui utilise la métaclasse réelle pour définir les méthodes singleton:
m = MyObject.new
metaclass = class << m; self; end
metaclass.send :attr_accessor, :first, :second
m.first = "first"
m.second = "second"
puts m.first, m.second
Vous pouvez utiliser à la fois la métaclasse et les classes ouvertes pour devenir encore plus compliqué et faire quelque chose comme:
class MyObject
def metaclass
class << self
self
end
end
def define_attributes(hash)
hash.each_pair { |key, value|
metaclass.send :attr_accessor, key
send "#{key}=".to_sym, value
}
end
end
m = MyObject.new
m.define_attributes({ :first => "first", :second => "second" })
Ce qui précède consiste essentiellement à exposer la métaclasse via la méthode "metaclass", puis à l'utiliser dans define_attributes pour définir dynamiquement un groupe d'attributs avec attr_accessor, puis à invoquer ensuite l'attribut setter avec la valeur associée dans le hachage.
Avec Ruby vous pouvez faire preuve de créativité et faire la même chose de différentes manières ;-)
Pour info, au cas où vous ne le sauriez pas, utiliser la métaclasse comme je l'ai fait signifie que vous n'agissez que sur l'instance donnée de l'objet. Ainsi, l'invocation de define_attributes ne définira que ces attributs pour cette instance particulière.
Exemple:
m1 = MyObject.new
m2 = MyObject.new
m1.define_attributes({:a => 123, :b => 321})
m2.define_attributes({:c => "abc", :d => "zxy"})
puts m1.a, m1.b, m2.c, m2.d # this will work
m1.c = 5 # this will fail because c= is not defined on m1!
m2.a = 5 # this will fail because a= is not defined on m2!
réponse de Mike Stone est déjà assez complet, mais j'aimerais ajouter un petit détail.
Vous pouvez modifier votre classe à tout moment, même après la création d'une instance, et obtenir les résultats souhaités. Vous pouvez l'essayer dans votre console:
s1 = 'string 1'
s2 = 'string 2'
class String
attr_accessor :my_var
end
s1.my_var = 'comment #1'
s2.my_var = 'comment 2'
puts s1.my_var, s2.my_var
Les autres solutions fonctionneront également parfaitement, mais voici un exemple utilisant define_method, si vous êtes déterminé à ne pas utiliser de classes ouvertes ... il définira la variable "var" pour la classe array ... mais notez qu'elle est ÉQUIVALENTE à utiliser une classe ouverte ... l'avantage est que vous pouvez le faire pour une classe inconnue (donc la classe de n'importe quel objet, plutôt que d'ouvrir une classe spécifique) ... define_method fonctionnera également à l'intérieur d'une méthode, alors que vous ne pouvez pas ouvrir une classe à l'intérieur une méthode.
array = []
array.class.send(:define_method, :var) { @var }
array.class.send(:define_method, :var=) { |value| @var = value }
Et voici un exemple de son utilisation ... notez que array2, un DIFFÉRENT tableau a également les méthodes, donc si ce n'est pas ce que vous voulez, vous veulent probablement des méthodes singleton que j'ai expliquées dans un autre post.
irb(main):001:0> array = []
=> []
irb(main):002:0> array.class.send(:define_method, :var) { @var }
=> #<Proc:0x00007f289ccb62b0@(irb):2>
irb(main):003:0> array.class.send(:define_method, :var=) { |value| @var = value }
=> #<Proc:0x00007f289cc9fa88@(irb):3>
irb(main):004:0> array.var = 123
=> 123
irb(main):005:0> array.var
=> 123
irb(main):006:0> array2 = []
=> []
irb(main):007:0> array2.var = 321
=> 321
irb(main):008:0> array2.var
=> 321
irb(main):009:0> array.var
=> 123
J'ai écrit un joyau pour cela il y a quelque temps. Il s'appelle "Flexible" et n'est pas disponible via rubygems, mais était disponible via github jusqu'à hier. Je l'ai supprimé car il était inutile pour moi.
Tu peux faire
class Foo
include Flexible
end
f = Foo.new
f.bar = 1
avec elle sans obtenir aucune erreur. Vous pouvez donc définir et obtenir des variables d'instance à partir d'un objet à la volée. Si vous êtes intéressé ... je pourrais télécharger à nouveau le code source sur github. Il a besoin de quelques modifications pour permettre
f.bar?
#=> true
comme méthode pour demander à l'objet si une variable d'instance "bar" est définie ou non, mais que tout le reste est en cours d'exécution.
Cordialement, musicmatze
Il semble que toutes les réponses précédentes supposent que vous savez quel est le nom de la classe que vous souhaitez modifier lorsque vous écrivez votre code. Eh bien, ce n'est pas toujours vrai (du moins, pas pour moi). Je suis peut-être en train d'itérer sur un tas de classes sur lesquelles je veux attribuer une variable (par exemple, pour contenir des métadonnées ou quelque chose). Dans ce cas, quelque chose comme ça fera le travail,
# example classes that we want to Tweak
class Foo;end
class Bar;end
klasses = [Foo, Bar]
# iterating over a collection of klasses
klasses.each do |klass|
# #class_eval gets it done
klass.class_eval do
attr_accessor :baz
end
end
# it works
f = Foo.new
f.baz # => nil
f.baz = 'it works' # => "it works"
b = Bar.new
b.baz # => nil
b.baz = 'it still works' # => "it still works"
En lecture seule, en réponse à votre modification:
Edit: Il semble que je doive clarifier que je recherche une solution de métaprogrammation qui me permette de modifier l'instance de classe au moment de l'exécution au lieu de modifier le code source qui définissait à l'origine la classe. Quelques-unes des solutions expliquent comment déclarer des variables d'instance dans les définitions de classe, mais ce n'est pas ce que je demande. Désolé pour la confusion.
Je pense que vous ne comprenez pas très bien le concept de "classes ouvertes", ce qui signifie que vous pouvez ouvrir une classe à tout moment. Par exemple:
class A
def hello
print "hello "
end
end
class A
def world
puts "world!"
end
end
a = A.new
a.hello
a.world
Ce qui précède est parfaitement valide Ruby code, et les 2 définitions de classe peuvent être réparties sur plusieurs fichiers Ruby. Vous pouvez utiliser la méthode "define_method" dans le module pour définir une nouvelle méthode sur une instance de classe, mais cela équivaut à utiliser des classes ouvertes.
"Ouvrir des classes" dans Ruby signifie que vous pouvez redéfinir N'IMPORTE QUELLE classe à TOUT moment ... ce qui signifie ajouter de nouvelles méthodes, redéfinir les méthodes existantes, ou tout ce que vous voulez vraiment. Cela ressemble à " la solution "open class" est vraiment ce que vous cherchez ...