Ajouter une variable d'instance à une classe dans Ruby
Comment puis-je ajouter une variable d'instance à une classe définie à runtime , et plus tard obtenir et définir sa valeur à partir de l'extérieur de la classe?
je suis à la recherche d'une solution de métaprogrammation qui me permet de modifier l'instance de classe à l'exécution au lieu de modifier le code source qui a défini à l'origine la classe. Quelques-unes des solutions expliquent comment déclarer les variables d'instance dans les définitions de classe, mais ce n'est pas ce que je demande.
8 réponses
vous pouvez utiliser attribut accessors:
class Array
attr_accessor :var
end
Maintenant vous pouvez 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 juste getters ou setters ou vous pouvez les définir manuellement comme tel:
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 les méthodes singleton si vous voulez qu'il soit défini sur une seule instance:
array = []
def array.var
@var
end
def array.var=(value)
@var = value
end
array.var = 123
puts array.var
FYI, en réponse au commentaire sur cette réponse, la méthode singleton fonctionne très bien, et ce qui suit 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
, que celui défini en utilisant le attr_accessor... une méthode simple est donc une approche parfaitement valable de cette question.
Ruby fournit des méthodes pour cela, instance_variable_get
et instance_variable_set
. ( docs )
vous pouvez créer et assigner de nouvelles variables d'instance comme ceci:
>> foo = Object.new
=> #<Object:0x2aaaaaacc400>
>> foo.instance_variable_set(:@bar, "baz")
=> "baz"
>> foo.inspect
=> #<Object:0x2aaaaaacc400 @bar=\"baz\">
@ Readonly
si votre utilisation de" class MyObject " est une utilisation d'une classe ouverte, veuillez noter que vous redéfinissez la méthode initialize.
dans Ruby, il n'y a pas de surcharge... seulement dominante, ou de redéfinition... en d'autres termes, il ne peut y avoir qu'une instance d'une méthode donnée, donc si vous la redéfinissez, elle est redéfinie... et la méthode initialize est pas différent (même si c'est ce que la nouvelle méthode D'utilisation des objets de classe).
ainsi, Ne jamais redéfinir une méthode existante sans l'identifier d'abord... au moins si vous voulez accéder à la définition originelle. Et redéfinir la méthode initialize d'une classe inconnue peut être très risqué.
en tout cas, je pense que j'ai 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 les deux classes metaclass et open pour obtenir encore plus difficile 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 ensemble d'attributs avec attr_accessor, puis à invoquer ensuite le setter d'attributs avec la valeur associée dans le hachage.
avec Ruby, vous pouvez être créatif et faire la même chose de nombreuses façons différentes ;-)
pour info, au cas où vous ne le saviez pas, utiliser la métaclasse comme je l'ai fait signifie que vous agissez seulement 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!
la réponse de Mike Stone est déjà très complète, mais j'aimerais ajouter un petit détail.
vous pouvez modifier votre classe à tout moment, même après qu'une instance ait été créée, et obtenir les résultats que vous désirez. 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 parfaitement aussi, Mais voici un exemple en utilisant define_method, si vous êtes bien décidé à ne pas utiliser les classes ouvertes... il définira la variable" var " pour la classe array... mais notez que C'est équivalent à 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)... aussi define_method va travailler à l'intérieur d'une méthode, alors que vous ne pouvez pas ouvrir une classe dans 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 tableau différent a aussi les méthodes, donc si ce n'est pas ce que vous voulez, vous voulez 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
Readonly, en réponse à votre edit:
Edit: il semble que j'ai besoin de clarifier que je cherche une métaprogrammation la solution qui me permet de modifier le instance de classe à l'exécution au lieu de modifier le code source que à l'origine, défini la classe. Un peu de les solutions expliquent comment déclarer variables d'instance dans la classe définitions, mais ce n'est pas ce que je suis en demandant au sujet. Désolé pour la confusion.
je pense que vous ne comprenez pas tout à fait 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
le code Ruby ci-dessus est parfaitement valide, et les 2 définitions de classe peuvent être étendues à plusieurs fichiers Ruby. Vous pouvez utiliser la méthode" define_method " dans L'objet Module pour définir une nouvelle méthode sur une instance de classe, mais elle est équivalente à utiliser des classes ouvertes.
Ouvert classes " dans Ruby signifie que vous pouvez redéfinir N'importe quelle classe à N'importe quel moment... ce qui signifie ajouter de nouvelles méthodes, redéfinir les méthodes existantes, ou tout ce que vous voulez vraiment. Il semble que la "classe ouverte" solution est vraiment ce que vous cherchez...
j'ai écrit un petit bijou pour ça 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 effacé parce que c'était inutile pour moi.
Vous pouvez le faire
class Foo
include Flexible
end
f = Foo.new
f.bar = 1
sans aucune erreur. De sorte que vous pouvez définir et obtenir des variables d'instance d'un objet à la volée. Si vous êtes intéressé... Je pourrais transférer le code source à github. Il a besoin de quelques modifications pour activer
f.bar?
#=> true
comme méthode pour demander à l'objet si une variable d'instance" bar " est définie ou non, mais n'importe quoi d'autre est en cours d'exécution.
salutations, musicmatze
Il semble que toutes les réponses précédentes supposent que vous savez quel est le nom de la catégorie que vous souhaitez modifier, c'est quand vous écrivez votre code. Ce n'est pas toujours vrai (du moins, pas pour moi). Je suis peut-être en train d'itérer sur une pile de classes que je veux attribuer une variable (par exemple, pour contenir des métadonnées ou quelque chose). Dans ce cas, quelque chose comme ça fera l'affaire,
# 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"