可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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).