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.

36
demandé sur the Tin Man 2008-09-30 04:44:22

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.

15
répondu Mike Stone 2017-02-08 22:35:02

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\">
63
répondu Gordon Wilson 2008-09-30 00:52:15

@ 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!
15
répondu Mike Stone 2017-05-23 11:55:13

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
2
répondu webmat 2017-05-23 10:29:33

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
2
répondu Mike Stone 2008-09-30 02:18:16

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

0
répondu Mike Stone 2008-09-30 01:44:46

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

0
répondu musicmatze 2012-11-11 20:28:43

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"
0
répondu Huliax 2015-10-20 18:34:30