Rechercher tous les descendants d'une classe dans Ruby

144

Je peux facilement monter dans la hiérarchie des classes dans Ruby:

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

Existe-t-il également un moyen de descendre la hiérarchie? J'aimerais faire ça

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

mais il ne semble pas y avoir de descendantsméthode.

(Cette question se pose parce que je veux trouver tous les modèles dans 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 avoir à modifier le contrôleur.)

Écureuil Douglas
la source

Réponses:

146

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
Petros
la source
1
Cela fonctionne très bien, merci! Je suppose que visiter chaque classe peut être trop lent si vous essayez de vous raser des millisecondes, mais c'est parfaitement rapide pour moi.
Douglas Squirrel
1
singleton_classau lieu de le Classrendre beaucoup plus rapide (voir la source sur apidock.com/rails/Class/descendants )
brauliobo
21
Faites attention si vous pourriez avoir une situation où la classe n'est pas encore chargée dans la mémoire, ObjectSpacene l'aura pas.
Edmund Lee
Comment pourrais-je faire en sorte que cela fonctionne pour Objectet BasicObject?, Curieux de savoir ce qu'ils montrent
Amol Pujari
@AmolPujari p ObjectSpace.each_object(Class)affichera toutes les classes. Vous pouvez également obtenir les descendants de toute classe souhaitée en remplaçant son nom selfdans la ligne de code de la méthode.
BobRodes
62

Si vous utilisez Rails> = 3, vous avez deux options en place. À utiliser .descendantssi vous voulez plus d'un niveau de profondeur des classes enfants, ou à utiliser .subclassespour le premier niveau des classes enfants.

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]
dgilperez
la source
6
Notez qu'en développement, si vous avez désactivé le chargement hâtif, ces méthodes ne renverront des 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).
stephen.hanson
1
@ stephen.hanson quel est le moyen le plus sûr de garantir des résultats corrects ici?
Chris Edwards
26

Ruby 1.9 (ou 1.8.7) avec des itérateurs chaînés astucieux:

#!/usr/bin/env ruby1.9

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

Ruby pré-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 ceci:

#!/usr/bin/env ruby

p Animal.descendants
Jörg W Mittag
la source
3
Cela fonctionne aussi pour les modules; remplacez simplement les deux instances de "Class" par "Module" dans le code.
korinthe
2
Pour plus de sécurité, il faut écrire ObjectSpace.each_object(::Class)- cela permettra au code de fonctionner lorsque vous aurez défini un YourModule :: Class.
Rene Saarsoo
19

Remplacez la méthode de classe nommée inherited . Cette méthode serait transmise à la sous-classe lors de sa création que vous pouvez suivre.

Chandra Sekar
la source
J'aime aussi celui-ci. Remplacer la méthode est légèrement intrusif, mais cela rend la méthode descendante un peu plus efficace car vous n'avez pas à visiter chaque classe.
Douglas Squirrel
@Douglas Bien qu'il soit moins intrusif, vous devrez probablement expérimenter pour voir s'il répond à vos besoins (c'est-à-dire quand Rails construit-il la hiérarchie contrôleur / modèle?).
Josh Lee
Il est également plus portable avec diverses implémentations ruby ​​non IRM, dont certaines ont une surcharge de performances importante due à l'utilisation d'ObjectSpace. La classe # inherited est idéale pour implémenter des modèles "d'enregistrement automatique" dans Ruby.
John Whitley
Envie de partager un exemple? Puisqu'il s'agit d'un niveau de classe, je suppose que vous devrez stocker chaque classe dans une sorte de variable globale?
Noz
@Noz Non, une variable d'instance sur la classe elle-même. Mais alors les objets ne peuvent pas être collectés par GC.
Phrogz
13

Alternativement (mis à jour pour ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Méthode compatible avec Ruby 1.8:

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

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

Apeiros
la source
c'était génial. Pouvez-vous m'expliquer comment cela fonctionne? J'ai utiliséObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West
1
Dans ruby ​​1.8, class<<some_obj;self;endrenvoie le singleton_class d'un objet. Dans la version 1.9+, vous pouvez utiliser à la some_obj.singleton_classplace (mis à jour ma réponse pour refléter cela). Chaque objet est une instance de sa singleton_class, qui s'applique également aux classes. Puisque each_object (SomeClass) renvoie toutes les instances de SomeClass, et SomeClass est une instance de SomeClass.singleton_class, each_object (SomeClass.singleton_class) renverra SomeClass et toutes les sous-classes.
apeiros le
10

Bien que l'utilisation d'ObjectSpace fonctionne, la méthode de classe héritée semble mieux convenir ici documentation Ruby héritée (sous-classe)

Objectspace est essentiellement un moyen d'accéder à tout et à tout ce qui utilise actuellement la mémoire allouée, donc itérer sur chacun de ses éléments pour vérifier s'il s'agit d'une sous-classe de la classe Animal n'est pas idéal.

Dans le code ci-dessous, la méthode héritée de la classe Animal implémente un rappel qui ajoutera toute sous-classe nouvellement créée à son tableau de descendants.

class Animal
  def self.inherited(subclass)
    @descendants = []
    @descendants << subclass
  end

  def self.descendants
    puts @descendants 
  end
end
Mateo Sossah
la source
6
`@descendants || = []` sinon vous n'obtiendrez que le dernier descendant
Axel Tetzlaff
4

Je sais que vous demandez comment faire cela en héritage mais vous pouvez y parvenir directement dans Ruby en espaçant les noms de la classe ( Classou Module)

module DarthVader
  module DarkForce
  end

  BlowUpDeathStar = Class.new(StandardError)

  class Luck
  end

  class Lea
  end
end

DarthVader.constants  # => [:DarkForce, :BlowUpDeathStar, :Luck, :Lea]

DarthVader
  .constants
  .map { |class_symbol| DarthVader.const_get(class_symbol) }
  .select { |c| !c.ancestors.include?(StandardError) && c.class != Module }
  # => [DarthVader::Luck, DarthVader::Lea]

C'est beaucoup plus rapide de cette façon que de comparer à toutes les classes ObjectSpacecomme le proposent d'autres solutions.

Si vous en avez vraiment besoin dans un héritage, vous pouvez faire quelque chose comme ceci:

class DarthVader
  def self.descendants
    DarthVader
      .constants
      .map { |class_symbol| DarthVader.const_get(class_symbol) }
  end

  class Luck < DarthVader
    # ...
  end

  class Lea < DarthVader
    # ...
  end

  def force
    'May the Force be with you'
  end
end

benchmarks ici: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

mettre à jour

à la fin, tout ce que tu as à faire est ceci

class DarthVader
  def self.inherited(klass)
    @descendants ||= []
    @descendants << klass
  end

  def self.descendants
    @descendants || []
  end
end

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

merci @saturnflyer pour la suggestion

équivalent8
la source
3

(Rails <= 3.0) Vous pouvez également utiliser ActiveSupport :: DescendantsTracker pour faire l'acte. De la source:

Ce module fournit une implémentation interne pour suivre les descendants qui est plus rapide que l'itération dans ObjectSpace.

Comme il est bien modulaire, vous pouvez simplement choisir ce module particulier pour votre application Ruby.

chtrinh
la source
2

Ruby Facets a des descendants de classe #,

require 'facets/class/descendants'

Il prend également en charge un paramètre de distance générationnel.

trans
la source
2

Une version simple qui donne un tableau de tous les descendants d'une classe:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
Dorian
la source
1
Cela ressemble à une réponse supérieure. Malheureusement, il est toujours la proie du chargement paresseux des classes. Mais je pense qu'ils le font tous.
Dave Morse
@DaveMorse J'ai fini par lister les fichiers et charger manuellement les constantes pour les enregistrer en tant que descendants (puis j'ai fini par supprimer tout ça: D)
Dorian
1
Notez que cela #subclassesprovient de Rails ActiveSupport.
Alex D
1

Vous pouvez require 'active_support/core_ext'et utilisez la descendantsméthode. Consultez le document et essayez-le en IRB ou en faisant levier. Peut être utilisé sans rails.

thelostspore
la source
1
Si vous devez ajouter un support actif à votre Gemfile, alors ce n'est pas vraiment "sans rails". Il suffit de choisir les morceaux de rails que vous aimez.
Caleb
1
Cela semble être une tangente philosophique qui n'est pas pertinente pour le sujet ici, mais je pense que l'utilisation d'un composant Rails signifie nécessairement qu'il est using Railsdans un sens holistique.
thelostspore
0

Rails fournit une méthode de sous-classes pour chaque objet, mais elle n'est pas bien documentée et je ne sais pas où elle est définie. Il renvoie un tableau de noms de classe sous forme de chaînes.

Daniel Tsadok
la source
0

L'utilisation du gem descendants_tracker peut aider. L'exemple suivant est copié à partir de la documentation du gem:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Cette gemme est utilisée par la gemme virtus populaire , donc je pense qu'elle est assez solide.

EricC
la source
0

Cette méthode retournera un hachage multidimensionnel de tous les descendants d'un Object.

def descendants_mapper(klass)
  klass.subclasses.reduce({}){ |memo, subclass|
    memo[subclass] = descendants_mapper(subclass); memo
  }
end

{ MasterClass => descendants_mapper(MasterClass) }
jozwright
la source
-1

Si vous avez accès au code avant le chargement d'une sous-classe, vous pouvez utiliser la méthode héritée .

Si vous ne le faites pas (ce qui n'est pas un cas mais cela pourrait être utile pour quiconque a trouvé ce message), vous pouvez simplement écrire:

x = {}
ObjectSpace.each_object(Class) do |klass|
     x[klass.superclass] ||= []
     x[klass.superclass].push klass
end
x[String]

Désolé si j'ai manqué la syntaxe mais l'idée doit être claire (je n'ai pas accès à ruby ​​pour le moment).

Maciej Piechotka
la source