37 votes

Ajouter une variable d'instance à une classe en Ruby

Comment puis-je ajouter une variable d'instance à une classe définie à l'exécution, et ensuite obtenir et définir sa valeur depuis l'extérieur de la classe?

Je recherche une solution de méta-programmation qui me permet de modifier l'instance de classe à l'exécution plutôt que de modifier le code source qui a originellement défini la classe. Certaines solutions expliquent comment déclarer des variables d'instance dans les définitions de classe, mais ce n'est pas ce dont je parle.

65voto

Gordon Wilson Points 14721

Ruby fournit des méthodes pour cela, instance_variable_get et instance_variable_set. (docs)

Vous pouvez créer et attribuer de nouvelles variables d'instance de cette manière :

>> foo = Object.new
=> #

>> foo.instance_variable_set(:@bar, "baz")
=> "baz"

>> foo.inspect
=> #

15voto

Mike Stone Points 21293

Vous pouvez utiliser des accesseurs d'attributs :

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 uniquement des getters ou des setters, ou vous pouvez les définir manuellement comme ceci :

class Array
  attr_reader :getter_only_method
  attr_writer :setter_only_method

  # Définitions manuelles équivalentes à l'utilisation de attr_reader/writer/accessor
  def var
    @var
  end

  def var=(valeur)
    @var = valeur
  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=(valeur)
  @var = valeur
end

array.var = 123
puts array.var

Pour votre information, en réponse au commentaire sur cette réponse, la méthode singleton fonctionne 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
=> #
irb(main):005:0> a.b = 1
=> 1
irb(main):006:0> a.b
=> 1
irb(main):007:0> def a.setit=(valeur)
irb(main):008:1>   @b = valeur
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 l'attr_accessor... donc une méthode singleton est une approche parfaitement valide pour cette question.

15voto

Mike Stone Points 21293

@Readonly

Si votre utilisation de "class MyObject" est une utilisation d'une classe ouverte, veuillez noter que vous redéfinissez la méthode initialize.

En Ruby, il n'y a pas de surcharge... seulement de remplacement, ou redéfinition... en d'autres termes, il ne peut y avoir qu'une seule instance de n'importe quelle méthode donnée, donc si vous la redéfinissez, elle est redéfinie... et la méthode initialize n'est pas différente (même si c'est ce que la nouvelle méthode des objets de classe utilise).

Ainsi, ne redéfinissez jamais une méthode existante sans lui donner un alias d'abord... du moins si vous voulez accéder à la définition originale. Et redéfinir la méthode initialize d'une classe inconnue peut être assez risqué.

Quoi qu'il en soit, je pense avoir une solution beaucoup plus simple pour vous, qui utilise la métaclass réelle pour définir des 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étaclass et les classes ouvertes pour devenir encore plus rusé 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" })

Le code ci-dessus expose essentiellement la métaclass via la méthode "metaclass", puis l'utilise dans define_attributes pour dynamiquement définir un tas d'attributs avec attr_accessor, et ensuite invoquer le setter d'attribut ensuite avec la valeur associée dans le hash.

Avec Ruby, vous pouvez être créatif et faire la même chose de nombreuses manières différentes ;-)


Petite info, au cas où vous ne le saviez pas, utiliser la métaclass comme je l'ai fait signifie que vous ne travaillez que sur l'instance donnée de l'objet. Ainsi, en invoquant define_attributes, vous allez seulement définir 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 # cela fonctionnera
m1.c = 5 # cela échouera car c= n'est pas défini sur m1 !
m2.a = 5 # cela échouera car a= n'est pas défini sur m2 !

2voto

webmat Points 13359

Réponse de Mike Stone est déjà assez complète, mais j'aimerais ajouter un petit détail.

Vous pouvez modifier votre classe à tout moment, même après la création de certaines instances, et obtenir les résultats souhaités. Vous pouvez essayer dans votre console :

s1 = 'string 1'
s2 = 'string 2'

class String
  attr_accessor :my_var
end

s1.my_var = 'commentaire #1'
s2.my_var = 'commentaire 2'

puts s1.my_var, s2.my_var

2voto

Mike Stone Points 21293

Les autres solutions fonctionneront parfaitement aussi, mais voici un exemple utilisant define_method, si vous êtes déterminé à ne pas utiliser les classes ouvertes... cela définira la variable "var" pour la classe des tableaux... mais notez que c'est ÉQUIVALENT à utiliser une classe ouverte... l'avantage est que vous pouvez le faire pour une classe inconnue (donc n'importe quelle classe d'objet, plutôt que d'ouvrir une classe spécifique)... et define_method fonctionnera à l'intérieur d'une méthode, alors que vous ne pouvez pas ouvrir une classe à l'intérieur d'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 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 }
=> #
irb(main):003:0> array.class.send(:define_method, :var=) { |value| @var = value }
=> #
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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X