web-dev-qa-db-fra.com

Ajout d'une variable d'instance à une classe dans Ruby

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.

37
Readonly

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.

15
Mike Stone

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\">
65
Gordon Wilson

@ lecture seule

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!
15
Mike Stone

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
2
webmat

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
2
Mike Stone

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

0
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"
0
Huliax

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 ...

0
Mike Stone