在 Ruby 中查找类的所有后代

我可以很容易地在 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 应用程序中从基类派生出来的所有模型并列出它们; 我有一个可以处理任何这样的模型的控制器,我希望能够添加新的模型而不必修改控制器

78506 次浏览

重写名为 继承的的类方法。这个方法将在创建子类时传递给您,您可以跟踪它。

如果您能够访问代码 之前,那么可以使用 继承的方法加载任何子类。

如果你没有(这不是一个例子,但它可能对任何发现这篇文章的人都有用) ,你可以写:

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

对不起,如果我错过了语法,但想法应该是明确的(我现在没有访问 Ruby)。

这里有一个例子:

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

后代给你:

GrandChild
Child

子孙后代给你:

GrandChild

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

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

像这样使用它:

#!/usr/bin/env ruby


p Animal.descendants

另外(更新为 ruby 1.9 +) :

ObjectSpace.each_object(YourRootClass.singleton_class)

Ruby 1.8兼容方式:

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

注意,这不适用于模块。此外,您的 RootClass 将包含在答案中。您可以使用 Array #-或者其他方法来删除它。

(Rails < = 3.0)或者你也可以使用 ActiveSupport: : DescendantsTracker 来完成这个任务。 来源:

这个模块提供了一个内部实现来跟踪子代,这比通过 ObjectSpace 迭代要快。

因为它已经很好地模块化了,所以您可以为您的 Ruby 应用程序“挑选”这个特定的模块。

Rails 为每个对象都提供了一个子类方法,但是没有很好的文档说明,我也不知道它是在哪里定义的。它以字符串的形式返回类名的数组。

Ruby Facets 有 Class # 的后代,

require 'facets/class/descendants'

它还支持一个代距离参数。

如果使用 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]

您可以使用 require 'active_support/core_ext'descendants方法。看看那个医生,并给它一个尝试在 IRB 或窥探。可以在不使用 Rails 的情况下使用。

我知道您正在询问如何在继承中实现这一点,但是您可以直接在 Ruby 中通过对类(ClassModule)使用名称间距来实现这一点

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]

谢谢你的建议

使用 追踪宝石可能会有所帮助:

class Foo
extend DescendantsTracker
end


class Bar < Foo
end


Foo.descendants # => [Bar]

这个宝石是由流行的 Virtus gem使用,所以我认为它是相当坚实的。

虽然使用 ObjectSpace 可以工作,但是继承的类方法似乎更适合这里的 继承的(子类) Ruby 文档

Objectspace 本质上是一种访问当前使用已分配内存的任何和所有内容的方法,因此迭代每个元素以检查它是否是 Animal 类的子类是不理想的。

在下面的代码中,继承的 Animal 类方法实现了一个回调,该回调将把任何新创建的子类添加到其子类数组中。

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


def self.descendants
puts @descendants
end
end

此方法将返回 Object 的所有子代的多维散列。

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


{ MasterClass => descendants_mapper(MasterClass) }

给出一个类的所有子类的数组的简单版本:

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

在其他答案的基础上(特别是那些推荐 subclassesdescendants的答案) ,您可能会发现在 Rails.env.development 中,事情会变得令人困惑。这是由于在开发过程中(默认情况下)急切加载被关闭。

如果你在 rails console中闲逛,你可以直接命名这个类,然后它就会被加载。从那时起,它将显示在 subclasses

在某些情况下,可能需要强制在代码中加载类。对于单表继承(Single Table Heritage,STI)来说尤其如此,因为您的代码很少直接提到子类。我曾经遇到过一两种情况,我不得不迭代所有的 STI 子类... ... 这在开发中并不能很好地工作。

下面是我为了开发而加载这些类的代码:

if Rails.env.development?
## These are required for STI and subclasses() to eager load in development:
require_dependency Rails.root.join('app', 'models', 'color', 'green.rb')
require_dependency Rails.root.join('app', 'models', 'color', 'blue.rb')
require_dependency Rails.root.join('app', 'models', 'color', 'yellow.rb')
end

之后,子类按预期工作:

> Color.subclasses
=> [Color::Green, Color::Blue, Color::Yellow]

注意,这在生产环境中是不需要的,因为所有的类都是预先加载的。

是的,这里有各种各样的代码味道。不管你接不接受... 它允许你在开发过程中停止急切的加载,同时仍然运行动态类操作。一旦处于激活状态,这对性能没有影响。

计算任意类的传递壳

def descendants(parent: Object)
outSet = []
lastLength = 0
     

outSet = ObjectSpace.each_object(Class).select { |child| child < parent }
     

return if outSet.empty?
     

while outSet.length == last_length
temp = []
last_length = outSet.length()
       

outSet.each do |parent|
temp = ObjectSpace.each_object(Class).select { |child| child < parent }
end
       

outSet.concat temp
outSet.uniq
temp = nil
end
outSet
end
end

Class # 子类(Ruby 3.1 +)

从 Ruby 3.1开始,班级 # 后代是一种内置方法。

它返回一个类的所有子类(不包括接收方类和单例类)。

因此,不再需要依赖 ActiveSupport 或编写修补程序来使用它。

class A; end
class B < A; end
class C < B; end


A.descendants    #=> [B, C]
B.descendants    #=> [C]
C.descendants    #=> []

资料来源:

对于 Ruby 3.1 + Class # 子类是可用的。 Class # 子类是 没有实现的:

class A; end
class B < A; end
class C < B; end
class D < A; end


A.subclasses => [B, D]


A.descendants => NoMethodError: undefined method 'descendants' for A:Class


A.methods.grep('descendants') => []


对于 Ruby < 3.1,这比 Rails 实现稍微快一点:

def descendants
ObjectSpace.each_object(singleton_class).reduce([]) do |des, k|
des.unshift k unless k.singleton_class? || k == self
des
end
end

Ruby 3.1 + # 子类看起来比上面给出的子代方法快得多。