Посмотрите всех потомков класса в Ruby

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, интересно знать , что они появляются?
Āmol Pujari
@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]
dgilperez
источник
6
Обратите внимание, что в процессе разработки, если у вас активная загрузка отключена, эти методы будут возвращать классы только в том случае, если они были загружены (т.е. если на них уже ссылался работающий сервер).
Стивен Хансон,
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" в коде.
Коринф
2
Для большей безопасности следует написать ObjectSpace.each_object(::Class)- это будет поддерживать работоспособность кода, когда у вас определен класс YourModule :: Class.
Рене Саарсоо
19

Переопределить метод класса с именем унаследованного . Этот метод будет передан подклассу при его создании, который вы можете отслеживать.

Чандра Секар
источник
Мне тоже нравится этот Переопределение метода незначительно навязчиво, но делает метод-потомок немного более эффективным, поскольку вам не нужно посещать каждый класс.
Белка Дугласа
@Douglas Хотя это менее навязчиво, вам, вероятно, придется поэкспериментировать, чтобы увидеть, отвечает ли оно вашим потребностям (то есть когда Rails строит иерархию контроллер / модель?).
Джош Ли
Он также более переносим к различным реализациям ruby ​​без MRI, некоторые из которых имеют серьезные потери производительности при использовании ObjectSpace. Унаследованный класс # идеален для реализации шаблонов "автоматической регистрации" в Ruby.
Джон Уитли
Хотите поделиться примером? Так как это уровень класса, я думаю, вам придется хранить каждый класс в какой-то глобальной переменной?
Noz
@ Noz Нет, переменная экземпляра в самом классе. Но тогда объекты не могут быть собраны GC.
Phrogz
13

Альтернативно (обновлено для ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Ruby 1.8 совместимый способ:

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

Обратите внимание, что это не будет работать для модулей. Также YourRootClass будет включен в ответ. Вы можете использовать Array # - или другой способ удалить его.

apeiros
источник
это было потрясающе. Можете ли вы объяснить мне, как это работает? Я использовалObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
Дэвид Уэст
1
В ruby ​​1.8 class<<some_obj;self;endвозвращает singleton_class объекта. В 1.9+ вы можете использовать some_obj.singleton_classвместо этого (обновил мой ответ, чтобы отразить это). Каждый объект является экземпляром своего класса singleton_class, который также применяется к классам. Поскольку each_object (SomeClass) возвращает все экземпляры SomeClass, а SomeClass является экземпляром SomeClass.singleton_class, each_object (SomeClass.singleton_class) возвращает SomeClass и все подклассы.
Апейрос
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 за предложение

equivalent8
источник
3

(Rails <= 3.0) В качестве альтернативы вы можете использовать ActiveSupport :: DescendantsTracker, чтобы сделать дело. Из источника:

Этот модуль обеспечивает внутреннюю реализацию для отслеживания потомков, что быстрее, чем итерация в ObjectSpace.

Поскольку он хорошо модульный, вы можете просто «выбрать вишню» для этого конкретного модуля для вашего приложения Ruby.

chtrinh
источник
2

В Ruby Facets есть потомки класса #,

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 или pry. Может использоваться без Rails.

thelostspore
источник
1
Если вам нужно добавить активную поддержку в ваш Gemfile, то это не совсем «без рельсов». Это просто выбор кусков рельсов, которые вам нравятся.
Калеб
1
Это похоже на философский аспект, который не имеет отношения к теме здесь, но я думаю, что использование компонента Rails обязательно означает, что он using Railsв целостном смысле.
thelostspore
0

Rails предоставляет метод подклассов для каждого объекта, но он плохо документирован, и я не знаю, где он определен. Возвращает массив имен классов в виде строк.

Даниэль Цадок
источник
0

Может помочь использование gem downndants_tracker . Следующий пример скопирован из документа gem:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Этот драгоценный камень используется популярным драгоценным камнем виртус , поэтому я думаю, что он довольно солидный.

EricC
источник
0

Этот метод вернет многомерный хеш всех потомков объекта.

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

{ MasterClass => descendants_mapper(MasterClass) }
jozwright
источник
-1

Если у вас есть доступ к коду до загрузки какого-либо подкласса, вы можете использовать унаследованный метод.

Если вы этого не сделаете (что не так, но это может быть полезно для всех, кто нашел этот пост), вы можете просто написать:

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

Извините, если я пропустил синтаксис, но идея должна быть ясной (в данный момент у меня нет доступа к ruby).

Мацей Печотка
источник