Suchen Sie in Ruby nach allen Nachkommen einer Klasse

144

Ich kann die Klassenhierarchie in Ruby leicht aufsteigen lassen:

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

Gibt es eine Möglichkeit, die Hierarchie ebenfalls abzusteigen? Ich würde das gerne machen

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

aber es scheint keine descendantsMethode zu geben.

(Diese Frage wird gestellt, weil ich alle Modelle in einer Rails-Anwendung finden möchte, die von einer Basisklasse abstammen, und sie auflisten möchte. Ich habe einen Controller, der mit jedem solchen Modell arbeiten kann, und ich möchte neue Modelle hinzufügen können ohne den Controller ändern zu müssen.)

Douglas Eichhörnchen
quelle

Antworten:

146

Hier ist ein Beispiel:

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

setzt Parent.descendants gibt Ihnen:

GrandChild
Child

setzt Child.descendants gibt Ihnen:

GrandChild
Petros
quelle
1
Das funktioniert super, danke! Ich nehme an, dass der Besuch jeder Klasse zu langsam ist, wenn Sie versuchen, Millisekunden zu rasieren, aber für mich ist es vollkommen schnell.
Douglas Squirrel
1
singleton_classanstatt Classes viel schneller zu machen (siehe Quelle unter apidock.com/rails/Class/descendants )
brauliobo
21
Seien Sie vorsichtig, wenn Sie eine Situation haben, in der die Klasse noch nicht in den Speicher geladen ist und ObjectSpacediese nicht hat.
Edmund Lee
Wie könnte ich diese Arbeit für Objectund machen BasicObject?, Neugierig zu wissen, was sie zeigen
Amol Pujari
@AmolPujari p ObjectSpace.each_object(Class)druckt alle Klassen aus. Sie können die Nachkommen jeder gewünschten Klasse auch abrufen, indem Sie ihren Namen selfin der Codezeile der Methode ersetzen .
BobRodes
62

Wenn Sie Rails> = 3 verwenden, haben Sie zwei Möglichkeiten. Verwenden .descendantsSie diese Option, wenn Sie mehr als eine Ebenentiefe für untergeordnete Klassen oder .subclassesfür die erste Ebene von untergeordneten Klassen verwenden möchten .

Beispiel:

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
quelle
6
Beachten Sie, dass diese Methoden in der Entwicklung, wenn Sie das eifrige Laden deaktiviert haben, Klassen nur zurückgeben, wenn sie geladen wurden (dh wenn sie bereits vom laufenden Server referenziert wurden).
stephen.hanson
1
@ stephen.hanson Was ist der sicherste Weg, um hier korrekte Ergebnisse zu garantieren?
Chris Edwards
26

Ruby 1.9 (oder 1.8.7) mit raffinierten verketteten Iteratoren:

#!/usr/bin/env ruby1.9

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

Ruby vor 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

Verwenden Sie es so:

#!/usr/bin/env ruby

p Animal.descendants
Jörg W Mittag
quelle
3
Dies funktioniert auch für Module. Ersetzen Sie einfach beide Instanzen von "Class" durch "Module" im Code.
Korinthe
2
Für zusätzliche Sicherheit sollte man schreiben ObjectSpace.each_object(::Class)- dies sorgt dafür, dass der Code funktioniert, wenn zufällig eine YourModule :: -Klasse definiert ist.
Rene Saarsoo
19

Überschreiben Sie die Klassenmethode mit dem Namen geerbt . Diese Methode wird beim Erstellen der Unterklasse übergeben, die Sie verfolgen können.

Chandra Sekar
quelle
Ich mag diesen auch. Das Überschreiben der Methode ist geringfügig aufdringlich, macht die Nachkommenmethode jedoch etwas effizienter, da Sie nicht jede Klasse besuchen müssen.
Douglas Squirrel
@Douglas Obwohl es weniger aufdringlich ist, müssen Sie wahrscheinlich experimentieren, um festzustellen, ob es Ihren Anforderungen entspricht (dh wann erstellt Rails die Controller- / Modellhierarchie?).
Josh Lee
Es ist auch portabler für verschiedene Nicht-MRT-Ruby-Implementierungen, von denen einige durch die Verwendung von ObjectSpace einen erheblichen Leistungsaufwand verursachen. Die geerbte Klasse # ist ideal für die Implementierung von Mustern für die automatische Registrierung in Ruby.
John Whitley
Möchtest du ein Beispiel teilen? Da es sich um eine Klassenstufe handelt, müssten Sie wahrscheinlich jede Klasse in einer Art globaler Variable speichern?
Noz
@Noz Nein, eine Instanzvariable für die Klasse selbst. Aber dann können die Objekte nicht von GC gesammelt werden.
Phrogz
13

Alternativ (aktualisiert für Ruby 1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Ruby 1.8 kompatibler Weg:

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

Beachten Sie, dass dies für Module nicht funktioniert. Außerdem wird YourRootClass in die Antwort aufgenommen. Sie können Array # - oder eine andere Methode zum Entfernen verwenden.

Apeiros
quelle
das war wunderbar. Können Sie mir erklären, wie das funktioniert? Ich benutzteObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West
1
Gibt in Ruby 1.8 class<<some_obj;self;enddie singleton_class eines Objekts zurück. In 1.9+ können Sie some_obj.singleton_classstattdessen verwenden (meine Antwort wurde aktualisiert, um dies widerzuspiegeln). Jedes Objekt ist eine Instanz seiner singleton_class, die auch für Klassen gilt. Da jedes Objekt (SomeClass) alle Instanzen von SomeClass zurückgibt und SomeClass eine Instanz von SomeClass.singleton_class ist, gibt jedes Objekt (SomeClass.singleton_class) SomeClass und alle Unterklassen zurück.
Apeiros
10

Obwohl die Verwendung von ObjectSpace funktioniert, scheint die geerbte Klassenmethode hier besser für die geerbte Ruby-Dokumentation (Unterklasse) geeignet zu sein

Objectspace ist im Wesentlichen eine Möglichkeit, auf alles zuzugreifen, was derzeit den zugewiesenen Speicher verwendet. Daher ist es nicht ideal, jedes einzelne seiner Elemente zu durchlaufen, um zu überprüfen, ob es sich um eine Unterklasse der Animal-Klasse handelt.

Im folgenden Code implementiert die geerbte Animal-Klassenmethode einen Rückruf, der alle neu erstellten Unterklassen zu ihrem Nachkommen-Array hinzufügt.

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

  def self.descendants
    puts @descendants 
  end
end
Mateo Sossah
quelle
6
`@descendants || = []` sonst bekommst du nur den letzten Nachkommen
Axel Tetzlaff
4

Ich weiß, dass Sie fragen, wie dies bei der Vererbung zu tun ist, aber Sie können dies direkt in Ruby erreichen, indem Sie die Klasse ( Classoder Module) mit einem Namensabstand versehen.

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]

Auf diese Weise ist es viel schneller als im Vergleich zu jeder Klasse in ObjectSpace wie es andere Lösungen vorschlagen.

Wenn Sie dies in einer Erbschaft ernsthaft benötigen, können Sie Folgendes tun:

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 hier: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

aktualisieren

Am Ende müssen Sie nur noch Folgendes tun

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

danke @saturnflyer für den vorschlag

Äquivalent8
quelle
3

(Rails <= 3.0) Alternativ können Sie ActiveSupport :: DescendantsTracker verwenden, um die Tat auszuführen. Aus der Quelle:

Dieses Modul bietet eine interne Implementierung zum Verfolgen von Nachkommen, die schneller ist als das Durchlaufen von ObjectSpace.

Da es gut modularisiert ist, können Sie einfach das jeweilige Modul für Ihre Ruby-App auswählen.

chtrinh
quelle
2

Ruby Facets hat Nachkommen der Klasse #,

require 'facets/class/descendants'

Es unterstützt auch einen Generationsabstandsparameter.

trans
quelle
2

Eine einfache Version, die ein Array aller Nachkommen einer Klasse enthält:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
Dorian
quelle
1
Dies scheint eine überlegene Antwort zu sein. Leider fällt es immer noch dem faulen Laden von Klassen zum Opfer. Aber ich denke, das tun sie alle.
Dave Morse
@ DaveMorse Am Ende habe ich Dateien aufgelistet und die Konstanten manuell geladen, um sie als Nachkommen zu registrieren (und dann das Ganze entfernt: D)
Dorian
1
Beachten Sie, dass dies #subclassesvon Rails ActiveSupport stammt.
Alex D
1

Sie können require 'active_support/core_ext'und verwenden die descendantsMethode. Schauen Sie sich das Dokument an und probieren Sie es in IRB oder Pry aus. Kann ohne Schienen verwendet werden.

die letzte Spore
quelle
1
Wenn Sie Ihrem Gemfile aktive Unterstützung hinzufügen müssen, ist es nicht wirklich "ohne Schienen". Es geht nur darum, die Schienenstücke auszuwählen, die Sie mögen.
Caleb
1
Dies scheint eine philosophische Tangente zu sein, die für das Thema hier nicht relevant ist, aber ich denke, dass die Verwendung einer Rails-Komponente notwendigerweise bedeutet, dass man using Railsin einem ganzheitlichen Sinne ist.
letzte Spore
0

Rails bietet für jedes Objekt eine Unterklassenmethode, die jedoch nicht gut dokumentiert ist, und ich weiß nicht, wo sie definiert ist. Es gibt ein Array von Klassennamen als Zeichenfolgen zurück.

Daniel Tsadok
quelle
0

Die Verwendung von descants_tracker gem kann helfen. Das folgende Beispiel wurde aus dem Dokument des Edelsteins kopiert:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Dieser Edelstein wird von dem beliebten Virtus-Edelstein verwendet , daher finde ich ihn ziemlich solide.

EricC
quelle
0

Diese Methode gibt einen mehrdimensionalen Hash aller Nachkommen eines Objekts zurück.

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

{ MasterClass => descendants_mapper(MasterClass) }
Jozwright
quelle
-1

Wenn Sie Zugriff auf Code haben, bevor eine Unterklasse geladen wird, können Sie geerbt verwenden Methode verwenden.

Wenn Sie dies nicht tun (was kein Fall ist, aber für jeden, der diesen Beitrag gefunden hat, nützlich sein könnte), können Sie einfach schreiben:

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

Es tut mir leid, wenn ich die Syntax verpasst habe, aber die Idee sollte klar sein (ich habe momentan keinen Zugriff auf Ruby).

Maciej Piechotka
quelle