152 votes

Recherche de tous les descendants d'une classe en Ruby

Je peux facilement remonter la hiérarchie des classes en Ruby :

String.ancestors     # [String, Enumerable, Comparable, Object, Kernel]
Enumerable.ancestors # [Enumerable]
Comparable.ancestors # [Comparable]
Object.ancestors     # [Object, Kernel]
Kernel.ancestors     # [Kernel]

Existe-t-il un moyen de descendre dans la hiérarchie ? J'aimerais faire ceci

Animal.descendants      # [Dog, Cat, Human, ...]
Dog.descendants         # [Labrador, GreatDane, Airedale, ...]
Enumerable.descendants  # [String, Array, ...]

mais il ne semble pas y avoir de descendants méthode.

(Cette question se pose parce que je veux trouver tous les modèles d'une application Rails qui descendent d'une classe de base et les lister ; j'ai un contrôleur qui peut fonctionner avec n'importe quel modèle de ce type et j'aimerais pouvoir ajouter de nouveaux modèles sans devoir modifier le contrôleur).

156voto

Petros Points 4430

Voici un exemple :

class Parent
  def self.descendants
    ObjectSpace.each_object(Class).select { |klass| klass < self }
  end
end

class Child < Parent
end

class GrandChild < Child
end

puts Parent.descendants
puts Child.descendants

met Parent.descendants vous donne :

GrandChild
Child

met Child.descendants vous donne :

GrandChild

1 votes

Cela fonctionne très bien, merci ! Je suppose que visiter chaque classe pourrait être trop lent si vous essayez de gagner des millisecondes, mais c'est parfaitement rapide pour moi.

1 votes

singleton_class au lieu de Class le rendre beaucoup plus rapide (voir la source à apidock.com/rails/Class/descendants )

27 votes

Faites attention si vous avez une situation où la classe n'est pas encore chargée dans la mémoire, ObjectSpace ne l'aura pas.

69voto

dgilperez Points 1021

Si vous utilisez Rails >= 3, vous avez deux options en place. Utiliser .descendants si vous voulez plus d'un niveau de profondeur des classes d'enfants, ou utilisez .subclasses pour le premier niveau de classes enfantines.

Exemple :

class Animal
end

class Mammal < Animal
end

class Dog < Mammal
end

class Fish < Animal
end

Animal.subclasses #=> [Mammal, Fish] 
Animal.descendants  #=> [Dog, Mammal, Fish]

9 votes

Notez qu'en développement, si vous avez désactivé le chargement rapide, ces méthodes ne renverront les classes que si elles ont été chargées (c'est-à-dire si elles ont déjà été référencées par le serveur en cours d'exécution).

4 votes

@stephen.hanson quel est le moyen le plus sûr de garantir des résultats corrects ici ?

0 votes

En mode développement, utilisez require_dependency pour autoloader tous les fichiers nécessaires AVANT .subclasses , .descendants est appelé. Pour obtenir les chemins nécessaires, utilisez un chemin ou un glob, tel que Rails.root.glob('pattern/to/*/file_*.rb) . Cela peut même être fait dans l'initialisateur comme expliqué dans une réponse ici : stackoverflow.com/questions/29662518/

26voto

Jörg W Mittag Points 153275

Ruby 1.9 (ou 1.8.7) avec de superbes itérateurs en chaîne :

#!/usr/bin/env ruby1.9

class Class
  def descendants
    ObjectSpace.each_object(::Class).select {|klass| klass < self }
  end
end

Ruby pre-1.8.7 :

#!/usr/bin/env ruby

class Class
  def descendants
    result = []
    ObjectSpace.each_object(::Class) {|klass| result << klass if klass < self }
    result
  end
end

Utilisez-le comme ça :

#!/usr/bin/env ruby

p Animal.descendants

3 votes

Cela fonctionne aussi pour les modules ; il suffit de remplacer les deux instances de "Class" par "Module" dans le code.

2 votes

Pour plus de sécurité, il faut écrire ObjectSpace.each_object(::Class) - cela permettra au code de continuer à fonctionner si vous avez défini un YourModule::Class.

22voto

Chandru Points 4770

Remplacez la méthode de la classe nommée hérité de . Cette méthode sera transmise à la sous-classe lors de sa création, ce que vous pouvez suivre.

0 votes

J'aime bien celui-là aussi. La surcharge de la méthode est légèrement intrusive, mais elle rend la méthode descendante un peu plus efficace puisque vous n'avez pas à visiter chaque classe.

0 votes

@Douglas Bien que ce soit moins intrusif, vous devrez probablement expérimenter pour voir si cela répond à vos besoins (par exemple, quand Rails construit-il la hiérarchie contrôleur/modèle ?)

0 votes

Elle est également plus portable vers diverses implémentations Ruby non IRM, dont certaines ont une sérieuse surcharge de performance due à l'utilisation d'ObjectSpace. Class#inherited est idéal pour mettre en œuvre les modèles d'"auto-enregistrement" en Ruby.

14voto

apeiros Points 898

Alternativement (mis à jour pour ruby 1.9+) :

ObjectSpace.each_object(YourRootClass.singleton_class)

Compatible avec Ruby 1.8 :

ObjectSpace.each_object(class<<YourRootClass;self;end)

Notez que cela ne fonctionne pas pour les modules. De plus, YourRootClass sera inclus dans la réponse. Vous pouvez utiliser Array#- ou un autre moyen pour le supprimer.

0 votes

C'était génial. Tu peux m'expliquer comment ça marche ? J'ai utilisé ObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}

1 votes

Dans ruby 1.8, class<<some_obj;self;end renvoie la singleton_class d'un objet. En 1.9+, vous pouvez utiliser some_obj.singleton_class à la place (j'ai mis à jour ma réponse pour refléter cela). Chaque objet est une instance de sa singleton_class, ce qui s'applique également aux classes. Puisque each_object(SomeClass) retourne toutes les instances de SomeClass, et que SomeClass est une instance de SomeClass.singleton_class, each_object(SomeClass.singleton_class) retournera SomeClass et toutes les sous-classes.

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