Look up all descendants of a class in Ruby

2019-01-02 22:42发布

问题:

I can easily ascend the class hierarchy in Ruby:

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

Is there any way to descend the hierarchy as well? I'd like to do this

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

but there doesn't seem to be a descendants method.

(This question comes up because I want to find all the models in a Rails application that descend from a base class and list them; I have a controller that can work with any such model and I'd like to be able to add new models without having to modify the controller.)

回答1:

Here is an example:

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

puts Parent.descendants gives you:

GrandChild
Child

puts Child.descendants gives you:

GrandChild


回答2:

If you use Rails >= 3, you have two options in place. Use .descendants if you want more than one level depth of children classes, or use .subclasses for the first level of child classes.

Example:

class Animal
end

class Mammal < Animal
end

class Dog < Mammal
end

class Fish < Animal
end

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


回答3:

Ruby 1.9 (or 1.8.7) with nifty chained iterators:

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

Use it like so:

#!/usr/bin/env ruby

p Animal.descendants


回答4:

Override the class method named inherited. This method would be passed the subclass when it is created which you can track.



回答5:

Alternatively (updated for ruby 1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Ruby 1.8 compatible way:

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

Note that this won't work for modules. Also, YourRootClass will be included in the answer. You can use Array#- or another way to remove it.



回答6:

Although using ObjectSpace works, the inherited class method seems to be better suitable here inherited(subclass) Ruby documentation

Objectspace is essentially a way to access anything and everything that's currently using allocated memory, so iterating over every single one of its elements to check if it is a sublass of the Animal class isn't ideal.

In the code below, the inherited Animal class method implements a callback that will add any newly created subclass to its descendants array.

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

  def self.descendants
    puts @descendants 
  end
end


回答7:

(Rails <= 3.0 ) Alternatively you could use ActiveSupport::DescendantsTracker to do the deed. From source:

This module provides an internal implementation to track descendants which is faster than iterating through ObjectSpace.

Since it is modularize nicely, you could just 'cherry-pick' that particular module for your Ruby app.



回答8:

I know you are asking how to do this in inheritance but you can achieve this with directly in Ruby by name-spacing the class (Class or 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]

It's much faster this way than comparing to every class in ObjectSpace like other solutions propose.

If you seriously need this in a inheritance you can do something like this:

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

update

in the end all you have to do is this

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

thank you @saturnflyer for suggestion



回答9:

Ruby Facets has Class#descendants,

require 'facets/class/descendants'

It also supports a generational distance parameter.



回答10:

A simple version that give an array of all the descendants of a class:

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


回答11:

You can require 'active_support/core_ext' and use the descendants method. Check out the doc, and give it a shot in IRB or pry. Can be used without Rails.



回答12:

Rails provides a subclasses method for every object, but it's not well documented, and I don't know where it's defined. It returns an array of class names as strings.



回答13:

Using descendants_tracker gem may help. The following example is copied from the gem's doc:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

This gem is used by the popular virtus gem, so I think it's pretty solid.



回答14:

This method will return a multidimensional hash of all of an Object's descendants.

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

{ MasterClass => descendants_mapper(MasterClass) }


回答15:

If you have access to code before any subclass is loaded then you can use inherited method.

If you don't (which is not a case but it might be useful for anyone who found this post) you can just write:

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

Sorry if I missed the syntax but idea should be clear (I don't have access to ruby at this moment).



标签: