Cerca tutti i discendenti di una classe in Ruby


144

Posso facilmente risalire alla gerarchia di classi in Ruby:

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

C'è un modo per discendere anche nella gerarchia? Mi piacerebbe farlo

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

ma non sembra esserci un descendantsmetodo.

(Questa domanda sorge perché voglio trovare tutti i modelli in un'applicazione Rails che discendono da una classe base ed elencarli; ho un controller in grado di funzionare con qualsiasi modello e mi piacerebbe poter aggiungere nuovi modelli senza dover modificare il controller.)

Risposte:


146

Ecco un esempio:

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

mette Parent.descendants ti dà:

GrandChild
Child

mette Child.descendants ti dà:

GrandChild

1
Funziona benissimo, grazie! Immagino che visitare ogni lezione potrebbe essere troppo lento se stai cercando di radere millisecondi, ma è perfettamente veloce per me.
Douglas Squirrel,

1
singleton_classinvece di Classrenderlo molto più veloce (vedi fonte su apidock.com/rails/Class/descendants )
brauliobo

21
Fai attenzione se potresti avere una situazione in cui la classe non è ancora stata caricata in memoria, ObjectSpacenon l'avrai.
Edmund Lee,

Come potrei farlo funzionare per Objecte BasicObject?, Curioso di sapere cosa si presentano
Amol Pujari,

@AmolPujari p ObjectSpace.each_object(Class)stamperà tutte le classi. È inoltre possibile ottenere i discendenti di qualsiasi classe desiderata sottostutandone il nome selfnella riga di codice nel metodo.
BobRodes

62

Se si utilizza Rails> = 3, sono disponibili due opzioni. Utilizzare .descendantsse si desidera più di un livello di profondità delle classi per bambini o utilizzare .subclassesper il primo livello delle classi per bambini.

Esempio:

class Animal
end

class Mammal < Animal
end

class Dog < Mammal
end

class Fish < Animal
end

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

6
Si noti che in fase di sviluppo, se il caricamento desideroso è disattivato, questi metodi restituiranno le classi solo se sono stati caricati (ovvero se sono già stati indicati come riferimento dal server in esecuzione).
stephen.hanson,

1
@ stephen.hanson qual è il modo più sicuro per garantire risultati corretti qui?
Chris Edwards,

26

Ruby 1.9 (o 1.8.7) con eleganti iteratori concatenati:

#!/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

Usalo così:

#!/usr/bin/env ruby

p Animal.descendants

3
Questo funziona anche per i moduli; sostituisci entrambe le istanze di "Class" con "Module" nel codice.
korinthe,

2
Per maggiore sicurezza, si dovrebbe scrivere ObjectSpace.each_object(::Class): questo manterrà il codice funzionante quando si avrà una YourModule :: Class definita.
Rene Saarsoo,

19

Sostituisci il metodo di classe denominato ereditato . Questo metodo verrebbe passato alla sottoclasse al momento della creazione, che è possibile tenere traccia.


Anche a me piace questo. L'override del metodo è leggermente invadente, ma rende il metodo discendente un po 'più efficiente poiché non è necessario visitare ogni classe.
Douglas Squirrel,

@Douglas Sebbene sia meno invadente, probabilmente dovrai sperimentare per vedere se soddisfa le tue esigenze (cioè quando Rails costruisce la gerarchia del controller / modello?).
Josh Lee

È anche più portabile su varie implementazioni ruby ​​non MRI, alcune delle quali hanno un notevole sovraccarico di prestazioni dall'uso di ObjectSpace. Classe # ereditata è ideale per l'implementazione di schemi di "registrazione automatica" in Ruby.
John Whitley,

Vuoi condividere un esempio? Dal momento che è a livello di classe immagino che dovresti memorizzare ogni classe in una sorta di variabile globale?
Noz,

@Noz No, una variabile di istanza sulla classe stessa. Ma poi gli oggetti non possono essere raccolti da GC.
Phrogz,

13

In alternativa (aggiornato per ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Modo Ruby 1.8 compatibile:

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

Nota che questo non funzionerà per i moduli. Inoltre, YourRootClass sarà incluso nella risposta. Puoi usare Array # - o un altro modo per rimuoverlo.


è stato fantastico. Puoi spiegarmi come funziona? Ho usatoObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West,

1
In ruby ​​1.8, class<<some_obj;self;endrestituisce la singleton_class di un oggetto. In 1.9+ è possibile utilizzare some_obj.singleton_classinvece (aggiornata la mia risposta per riflettere quello). Ogni oggetto è un'istanza della sua singleton_class, che si applica anche alle classi. Poiché each_object (SomeClass) restituisce tutte le istanze di SomeClass e SomeClass è un'istanza di SomeClass.singleton_class, each_object (SomeClass.singleton_class) restituirà SomeClass e tutte le sottoclassi.
apeiros,

10

Sebbene l'utilizzo di ObjectSpace funzioni, il metodo di classe ereditato sembra essere più adatto qui documentazione Ruby ereditata (sottoclasse)

Lo spazio oggetti è essenzialmente un modo per accedere a tutto e a tutto ciò che attualmente utilizza la memoria allocata, quindi iterare su ciascuno dei suoi elementi per verificare se si tratta di una sottoclasse della classe Animal non è l'ideale.

Nel codice seguente, il metodo della classe Animal ereditato implementa un callback che aggiungerà qualsiasi sottoclasse appena creata alla sua matrice discendente.

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

  def self.descendants
    puts @descendants 
  end
end

6
`@descendants || = []` altrimenti otterrai solo l'ultimo discendente
Axel Tetzlaff,

4

So che stai chiedendo come farlo in eredità, ma puoi ottenerlo direttamente in Ruby spaziando il nome della classe ( Classo 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]

È molto più veloce in questo modo rispetto al confronto con ogni classe in ObjectSpace come altre soluzioni propongono.

Se ne hai davvero bisogno in eredità, puoi fare qualcosa del genere:

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

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

aggiornare

alla fine tutto ciò che devi fare è questo

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

grazie @saturnflyer per il suggerimento


3

(Rails <= 3.0) In alternativa, è possibile utilizzare ActiveSupport :: DescendantsTracker per eseguire l'operazione. Dalla fonte:

Questo modulo fornisce un'implementazione interna per tenere traccia dei discendenti che è più veloce dell'iterazione tramite ObjectSpace.

Dal momento che è modulare bene, potresti semplicemente scegliere "ciliegia" quel particolare modulo per la tua app Ruby.


2

Ruby Facets ha discendenti di classe #,

require 'facets/class/descendants'

Supporta anche un parametro di distanza generazionale.


2

Una versione semplice che fornisce una matrice di tutti i discendenti di una classe:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end

1
Questa sembra una risposta superiore. Sfortunatamente cade ancora in preda al pigro carico di lezioni. Ma penso che lo facciano tutti.
Dave Morse,

@DaveMorse Ho finito per elencare i file e caricare manualmente le costanti per averle registrate come discendenti (e poi ho finito per rimuovere tutto: D)
Dorian,

1
Si noti che #subclassesproviene da Rails ActiveSupport.
Alex D,

1

È possibile require 'active_support/core_ext'e utilizzare il descendantsmetodo Controlla il documento e provalo in IRB o fai leva. Può essere usato senza binari.


1
Se devi aggiungere un supporto attivo al tuo Gemfile, non è davvero "senza binari". Sta solo scegliendo i pezzi di binari che ti piacciono.
Caleb,

1
Sembra una tangente filosofica che non è rilevante per l'argomento qui, ma penso che l'uso di un componente Rails significhi necessariamente che uno è using Railsin senso olistico.
thoreostspore

0

Rails fornisce un metodo di sottoclassi per ogni oggetto, ma non è ben documentato e non so dove sia definito. Restituisce una matrice di nomi di classe come stringhe.


0

L'uso di descendants_tracker gem può essere d'aiuto. L'esempio seguente viene copiato dal documento della gemma:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Questa gemma viene utilizzata dalla famosa gemma virtus , quindi penso che sia piuttosto solida.


0

Questo metodo restituirà un hash multidimensionale di tutti i discendenti di un oggetto.

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

{ MasterClass => descendants_mapper(MasterClass) }

-1

Se si ha accesso al codice prima che venga caricata una sottoclasse, è possibile utilizzare il metodo ereditato .

Se non lo fai (il che non è un caso, ma potrebbe essere utile per chiunque abbia trovato questo post) puoi semplicemente scrivere:

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

Scusa se ho perso la sintassi, ma l'idea dovrebbe essere chiara (al momento non ho accesso a ruby).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.