Знайдіть усіх нащадків класу в Рубі


144

Я легко переходжу ієрархію класів у Ruby:

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

Чи є якийсь спосіб також зійти з ієрархії? Я хотів би це зробити

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

але, схоже, немає descendantsспособу.

(Це питання виникає, тому що я хочу знайти всі моделі в додатку Rails, що виходять з базового класу, і перерахувати їх; у мене є контролер, який може працювати з будь-якою такою моделлю, і я хотів би мати можливість додавати нові моделі без необхідності змінювати контролер.)

Відповіді:


146

Ось приклад:

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

ставить Parent.descendants дає вам:

GrandChild
Child

ставить Child.descendants дає вам:

GrandChild

1
Це чудово працює, дякую! Я вважаю, що відвідування кожного класу може бути надто повільним, якщо ви намагаєтесь поголити мілісекунди, але для мене це абсолютно швидко.
Дуглас Білка

1
singleton_classзамість того, Classщоб зробити це набагато швидше (див. джерело на apidock.com/rails/Class/descendants )
brauliobo

21
Будьте уважні, якщо у вас може виникнути ситуація, коли клас ще не завантажений у пам'ять, ObjectSpaceйого не буде.
Едмунд Лі

Як я міг змусити цю роботу Objectі BasicObject?, Цікаво дізнатися, що вони з’являються
Амол Пуджарі

@AmolPujari p ObjectSpace.each_object(Class)роздрукує всі класи. Ви також можете отримати нащадків будь-якого класу, який ви хочете, замінивши його ім'я selfв рядку коду методу.
BobRodes

62

Якщо ви використовуєте Rails> = 3, у вас є два варіанти. Використовуйте, .descendantsякщо ви хочете більше, ніж на рівні рівнів дітей, або використовуйте .subclassesдля дітей першого класу.

Приклад:

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
Зауважте, що в процесі розробки, якщо ви нетерплять завантаження, ці методи повертають класи лише у тому випадку, якщо вони завантажені (тобто якщо на них вже посилався запущений сервер).
stephen.hanson

1
@ stephen.hanson - який найбезпечніший спосіб гарантувати правильні результати тут?
Кріс Едвардс

26

Ruby 1.9 (або 1.8.7) з чудовими прикованими ітераторами:

#!/usr/bin/env ruby1.9

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

Рубін до 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

Використовуйте його так:

#!/usr/bin/env ruby

p Animal.descendants

3
Це працює і для Модулів; просто замініть обидва екземпляри "Class" на "Module" в коді.
korinthe

2
Для додаткової безпеки слід написати ObjectSpace.each_object(::Class)- це дозволить коду працювати, коли у вас буде визначений YourModule :: Class.
Рене Саарсоо

19

Перезазначте метод класу, названий успадкованим . Цей метод буде переданий підкласу, коли він створений, який ви можете відстежувати.


Мені це теж подобається. Перевизначення методу незначно нав'язливе, але це робить метод нащадка трохи ефективнішим, оскільки вам не доведеться відвідувати кожен клас.
Дуглас Білочка

@Douglas Хоча це менш настирливо, вам, ймовірно, доведеться експериментувати, щоб побачити, чи відповідає він вашим потребам (тобто коли Rails будує ієрархію контролера / моделі?).
Джош Лі

Він також більш портативний для різних не-MRI рубінових реалізацій, деякі з яких мають серйозні показники продуктивності від використання ObjectSpace. Клас # успадкований ідеально підходить для впровадження моделей "автоматичної реєстрації" в Ruby.
Джон Вітлі

Хочете поділитися прикладом? Оскільки це рівень класу, я думаю, вам доведеться зберігати кожен клас у якійсь глобальній змінній?
Ноз

@Noz Ні, змінна інстанція для самого класу. Але тоді об'єкти не можуть бути зібрані GC.
Фрогз

13

Як варіант (оновлено для ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Сумісний спосіб Ruby 1.8:

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

Зауважте, що це не працює для модулів. Також у відповідь буде включено YourRootClass. Ви можете використовувати Array # - або інший спосіб її видалення.


це було чудово. Чи можете ви пояснити мені, як це працює? Я використовувавObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
Девід Вест

1
У рубіні 1.8 class<<some_obj;self;endповертає singleton_class об'єкта. У 1.9+ ви можете використовувати some_obj.singleton_classзамість цього (оновлений мій відповідь, щоб це відобразити). Кожен об’єкт є екземпляром свого singleton_class, який застосовується і для класів. Оскільки every_object (SomeClass) повертає всі екземпляри SomeClass, а SomeClass є екземпляром SomeClass.singleton_class, every_object (SomeClass.singleton_class) повертає SomeClass та всі підкласи.
apeiros

10

Хоча використовуючи роботи ObjectSpace, метод спадкового класу, здається, краще підходить тут успадкованій (підклас) Ruby документації

Об'єктний простір - це, по суті, спосіб отримати доступ до будь-чого і всього, що зараз використовує виділену пам’ять, тому ітерація над кожним його елементом, щоб перевірити, чи це підклас класу Animal, не є ідеальним.

У наведеному нижче коді метод успадкованого класу Animal реалізує зворотний виклик, який додасть будь-який новостворений підклас до масиву його нащадків.

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

  def self.descendants
    puts @descendants 
  end
end

6
`@descendants || = []` в іншому випадку ви отримаєте лише останнього нащадка
Аксель Тецлафф

4

Я знаю, що ви запитуєте, як це зробити у спадок, але ви можете досягти цього безпосередньо в Ruby, розділяючи клас ( Classабо 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]

Це набагато швидше, ніж порівняння з кожним класом, ObjectSpaceяк пропонують інші рішення.

Якщо вам це серйозно потрібно в спадщину, ви можете зробити щось подібне:

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

орієнтири тут: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

оновлення

врешті-решт все, що вам потрібно зробити, це це

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

дякую @saturnflyer за пропозицію


3

(Rails <= 3.0) Або ви можете використовувати ActiveSupport :: DescendantsTracker, щоб зробити діло. З джерела:

Цей модуль забезпечує внутрішню реалізацію для відстеження нащадків, що швидше, ніж ітерація через ObjectSpace.

Оскільки це модуляризовано, ви можете просто «вишнево» вибрати саме цей модуль для додатка Ruby.


2

Рубін Фасет має нащадків класу #

require 'facets/class/descendants'

Він також підтримує генераційний параметр відстані.


2

Проста версія, яка дає масив усіх нащадків класу:

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

1
Це виглядає як чудова відповідь. На жаль, вона все ще стає здобиччю ледачого завантаження занять. Але я думаю, що вони все роблять.
Дейв Морз

@DaveMorse Я закінчив список файлів і завантажував константи вручну, щоб зареєструвати їх як нащадків (а потім видалив це все: D)
Доріан,

1
Зауважте, що #subclassesце від Rails ActiveSupport.
Алекс Д

1

Можна require 'active_support/core_ext'і використовувати descendantsметод. Ознайомтеся з документом і надішліть йому знімок в IRB або довідайтесь. Можна використовувати без рейок.


1
Якщо вам доведеться додати активну підтримку до свого Gemfile, то це насправді "без рейок". Це просто вибір тих рейок, які вам подобаються.
Калеб

1
Це здається філософською дотичною, що не стосується тут теми, але я думаю, що використання компонента Rails обов'язково означає, що це є using Railsв цілісному сенсі.
thelostspore

0

Rails надає метод підкласів для кожного об'єкта, але він недостатньо задокументований, і я не знаю, де це визначено. Він повертає масив назв класу у вигляді рядків.


0

Використання дорогоцінного каміння descendants_tracker може допомогти. Наступний приклад скопійовано з документа дорогоцінного каміння:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Цей дорогоцінний камінь використовується популярним самоцвітом virtus , тому я думаю, що це досить солідно.


0

Цей метод поверне багатовимірний хеш усіх нащадків Об'єкта.

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

{ MasterClass => descendants_mapper(MasterClass) }

-1

Якщо у вас є доступ до коду перед завантаженням будь-якого підкласу, ви можете використовувати успадкований метод.

Якщо ви цього не зробите (це не так, але це може бути корисно всім, хто знайшов цю публікацію), ви можете просто написати:

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

Вибачте, якщо я пропустив синтаксис, але ідея повинна бути зрозумілою (на даний момент у мене немає доступу до рубіну).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.