Wyszukaj wszystkich potomków klasy w języku Ruby


144

Mogę łatwo wspiąć się po hierarchii klas w Rubim:

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

Czy jest jakiś sposób na zejście również z hierarchii? Chciałbym to zrobić

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

ale wydaje się, że nie ma descendantsmetody.

(To pytanie pojawia się, ponieważ chcę znaleźć wszystkie modele w aplikacji Railsów, które pochodzą z klasy bazowej i je wyświetlić; Mam kontroler, który może pracować z każdym takim modelem i chciałbym móc dodawać nowe modele bez konieczności modyfikowania kontrolera.)

Odpowiedzi:


146

Oto przykład:

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

umieszcza Parent.descendants daje:

GrandChild
Child

stawia Child.descendants daje:

GrandChild

1
Działa świetnie, dzięki! Przypuszczam, że odwiedzanie wszystkich zajęć może być zbyt wolne, jeśli próbujesz golić się przez milisekundy, ale dla mnie jest to całkowicie szybkie.
Douglas Squirrel

1
singleton_classzamiast Classuczynić to znacznie szybszym (patrz źródło na apidock.com/rails/Class/descendants )
brauliobo

21
Uważaj, jeśli możesz mieć sytuację, w której klasa nie jest jeszcze załadowana do pamięci, ObjectSpacenie będzie jej.
Edmund Lee,

Jak mogę sprawić, by to działało dla Objecti BasicObject?,
Ciekawi

@AmolPujari p ObjectSpace.each_object(Class)wydrukuje wszystkie klasy. Możesz również pobrać elementy potomne dowolnej klasy, zastępując jej nazwę selfw wierszu kodu metody.
BobRodes

62

Jeśli używasz Rails> = 3, masz dwie opcje. Użyj, .descendantsjeśli chcesz mieć więcej niż jeden poziom głębokości klas podrzędnych lub użyj.subclasses podrzędnych dla pierwszego poziomu klas podrzędnych.

Przykład:

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
Zauważ, że w programowaniu, jeśli masz wyłączone ładowanie, metody te zwrócą klasy tylko wtedy, gdy zostały załadowane (tj. Jeśli były już przywoływane przez działający serwer).
stephen.hanson

1
@ stephen.hanson Jaki jest najbezpieczniejszy sposób zagwarantowania prawidłowych wyników tutaj?
Chris Edwards,

26

Ruby 1.9 (lub 1.8.7) ze sprytnymi iteratorami łańcuchowymi:

#!/usr/bin/env ruby1.9

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

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

Użyj go w ten sposób:

#!/usr/bin/env ruby

p Animal.descendants

3
Działa to również w przypadku modułów; po prostu zamień w kodzie oba wystąpienia „Class” na „Module”.
korinthe

2
Dla dodatkowego bezpieczeństwa należy napisać ObjectSpace.each_object(::Class)- dzięki temu kod będzie działał, gdy zdarzy się, że zdefiniujesz YourModule :: Class.
Rene Saarsoo

19

Zastąp metodę klasy o nazwie inherited . Ta metoda zostanie przekazana do podklasy podczas jej tworzenia, którą można śledzić.


To też mi się podoba. Zastępowanie metody jest nieznacznie inwazyjne, ale sprawia, że ​​metoda podrzędna jest nieco bardziej wydajna, ponieważ nie trzeba odwiedzać każdej klasy.
Douglas Squirrel

@Douglas Chociaż jest mniej inwazyjny, prawdopodobnie będziesz musiał poeksperymentować, aby sprawdzić, czy spełnia twoje potrzeby (tj. Kiedy Railsy budują hierarchię kontroler / model?).
Josh Lee,

Jest również bardziej przenośny do różnych implementacji rubinowych innych niż MRI, z których niektóre mają poważne obciążenie wydajnościowe wynikające z używania ObjectSpace. Dziedziczona klasa # jest idealna do implementacji wzorców „automatycznej rejestracji” w Rubim.
John Whitley,

Chcesz podzielić się przykładem? Ponieważ jest to poziom klasy, myślę, że musiałbyś przechowywać każdą klasę w jakiejś zmiennej globalnej?
Noz

@Noz Nie, zmienna instancji w samej klasie. Ale wtedy obiekty nie mogą być zebrane przez GC.
Phrogz

13

Alternatywnie (aktualizacja dla Ruby 1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Zgodność z Ruby 1.8:

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

Pamiętaj, że to nie zadziała dla modułów. W odpowiedzi znajdzie się również YourRootClass. Możesz użyć Array # - lub innego sposobu, aby go usunąć.


to było niesamowite. Czy możesz mi wyjaśnić, jak to działa? UżyłemObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West

1
W Ruby 1.8 class<<some_obj;self;endzwraca singleton_class obiektu. W wersji 1.9+ możesz użyć some_obj.singleton_classzamiast (zaktualizowałem moją odpowiedź, aby to odzwierciedlić). Każdy obiekt jest instancją swojej klasy singleton_class, która dotyczy również klas. Ponieważ each_object (SomeClass) zwraca wszystkie wystąpienia SomeClass, a SomeClass jest wystąpieniem klasy SomeClass.singleton_class, each_object (SomeClass.singleton_class) zwróci SomeClass i wszystkie podklasy.
apeiros

10

Chociaż używanie ObjectSpace działa, dziedziczona metoda klasy wydaje się być lepsza tutaj dziedziczona (podklasa) dokumentacja Rubiego

Przestrzeń obiektów to zasadniczo sposób na uzyskanie dostępu do czegokolwiek i wszystkiego, co aktualnie używa przydzielonej pamięci, więc iterowanie po każdym pojedynczym elemencie w celu sprawdzenia, czy jest to podklasa klasy Animal, nie jest idealne.

W poniższym kodzie odziedziczona metoda klasy Animal implementuje wywołanie zwrotne, które doda każdą nowo utworzoną podklasę do swojej tablicy potomków.

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

  def self.descendants
    puts @descendants 
  end
end

6
`@descendants || = []` w przeciwnym razie dostaniesz tylko ostatniego potomka
Axela Tetzlaffa

4

Wiem, że pytasz, jak to zrobić w dziedziczeniu, ale możesz to osiągnąć bezpośrednio w Rubim, oddzielając klasę ( Classlub 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]

W ten sposób jest znacznie szybszy niż w przypadku innych klas w ObjectSpace podobnie jak inne rozwiązania.

Jeśli naprawdę potrzebujesz tego w spadku, możesz zrobić coś takiego:

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

testy porównawcze tutaj: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

aktualizacja

w końcu wszystko, co musisz zrobić, to to

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

dziękuję @saturnflyer za sugestie


3

(Rails <= 3.0) Alternatywnie możesz użyć ActiveSupport :: DescendantsTracker do zrobienia tego. Ze źródła:

Ten moduł zapewnia wewnętrzną implementację do śledzenia potomków, która jest szybsza niż iteracja przez ObjectSpace.

Ponieważ jest ładnie modularyzowany, możesz po prostu wybrać ten konkretny moduł dla swojej aplikacji Ruby.


2

Aspekty Ruby mają potomków klasy #,

require 'facets/class/descendants'

Obsługuje również parametr odległości pokoleniowej.


2

Prosta wersja, która podaje tablicę wszystkich potomków klasy:

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

1
To wygląda na lepszą odpowiedź. Niestety nadal pada ofiarą leniwego ładowania zajęć. Ale myślę, że wszyscy to robią.
Dave Morse

@DaveMorse Skończyło się na tym, że wyszczególniłem pliki i ręcznie załadowałem stałe, aby zarejestrować je jako potomne (a potem skończyłem usunąć to wszystko: D)
Dorian

1
Zauważ, że #subclassespochodzi z Rails ActiveSupport.
Alex D

1

Możesz require 'active_support/core_ext'i użyj descendantsmetody. Sprawdź dokument i wypróbuj go w IRB lub podważ. Może być używany bez szyn.


1
Jeśli musisz dodać aktywne wsparcie do swojego Gemfile, to tak naprawdę nie jest "bez szyn". To po prostu wybór kawałków szyn, które lubisz.
Caleb,

1
Wydaje się, że jest to filozoficzna tangens, która nie ma związku z tym tematem, ale myślę, że użycie komponentu Rails musi koniecznie oznaczać, że jest to using Railsw sensie holistycznym.
thelostspore

0

Railsy zapewniają metodę podklas dla każdego obiektu, ale nie jest ona dobrze udokumentowana i nie wiem, gdzie jest zdefiniowana. Zwraca tablicę nazw klas jako ciągi.


0

Pomocne może być użycie klejnotu descendants_tracker . Poniższy przykład został skopiowany z dokumentu klejnotu:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Ten klejnot jest używany przez popularny klejnot virtus , więc myślę, że jest całkiem solidny.


0

Ta metoda zwróci wielowymiarowy skrót wszystkich potomków Object.

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

{ MasterClass => descendants_mapper(MasterClass) }

-1

Jeśli masz dostęp do kodu przed załadowaniem jakiejkolwiek podklasy, możesz użyć metody dziedziczonej .

Jeśli nie (co nie jest przypadkiem, ale może się przydać każdemu, kto znalazł ten post), możesz po prostu napisać:

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

Przepraszam, jeśli przegapiłem składnię, ale pomysł powinien być jasny (w tej chwili nie mam dostępu do ruby).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.