在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应用程序中找到所有模型并列出它们;我有一个可以与任何此类模型一起使用的控制器,并且我希望能够添加新模型无需修改控制器。)

Answers:


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,2015年

21
如果您可能遇到尚未ObjectSpace将类加载到内存中的情况,请小心。
Edmund Lee

我如何才能让这项工作ObjectBasicObject,好奇地想知道他们出现了什么?
阿莫尔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]

6
请注意,在开发中,如果您急切地希望关闭加载,则这些方法仅在加载了它们之后才返回类(即,如果正在运行的服务器已经引用了它们)。
stephen.hanson 18-10-11

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

Ruby 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
这也适用于模块。只需将代码中的两个“类”实例替换为“模块”即可。
korinthe 2011年

2
为了增加安全性,应该编写ObjectSpace.each_object(::Class)-碰巧定义了YourModule :: Class时,这可以使代码正常工作。
Rene Saarsoo 2011年

19

覆盖名为类方法继承。此方法将在创建时传递给子类,您可以对其进行跟踪。


我也喜欢这个。覆盖该方法在某种程度上是侵入式的,但是由于不必访问每个类,因此它使后代方法的效率更高。
道格拉斯松鼠

@Douglas虽然不那么麻烦,但您可能不得不尝试看看它是否满足您的需求(即Rails何时构建控制器/模型层次结构?)。
Josh Lee 2010年

它也更易于移植到各种非MRI红宝石实现中,其中一些由于使用ObjectSpace而具有严重的性能开销。继承于Class#inherited是在Ruby中实现“自动注册”模式的理想选择。
约翰·惠特利

愿意分享一个例子吗?由于它是类级别的,我想您必须将每个类存储在某种全局变量中?
Noz 2014年

@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#-或其他方法将其删除。


这太棒了。你能告诉我这是怎么回事吗?我曾经用过ObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West

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和所有子类。
apeiros 2014年

10

尽管使用ObjectSpace可以工作,但是继承的类方法似乎更适合此处的继承(子类)Ruby文档

从本质上讲,对象空间是一种访问当前正在使用分配的内存的所有内容的方法,因此遍历其元素的每个元素以检查它是否是Animal类的子集是不理想的。

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

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

  def self.descendants
    puts @descendants 
  end
end

6
`@descendants || = []`否则,您只会得到最后一个后裔
Axel Tetzlaff

4

我知道您在问如何在继承中执行此操作,但是您可以通过在类中使用名称间隔(ClassModule)直接在Ruby中实现此功能

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

Ruby Facets具有Class#descendant,

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
这看起来是一个更好的答案。不幸的是,它仍然是延迟加载类的牺牲品。但是我认为它们都可以。
Dave Morse

@DaveMorse我最终列出文件并手动加载常量以使它们注册为后代(然后最终删除了整个内容:D)
Dorian

1
请注意,#subclasses它来自Rails ActiveSupport。
Alex D

1

您可以require 'active_support/core_ext'并使用该descendants方法。签出该文档,然后尝试使用 IRB或撬动该文档。可以在没有Rails的情况下使用。


1
如果您必须向Gemfile添加主动支持,那么它并不是真正的“无轨”。只是选择您喜欢的导轨。
卡雷布(Caleb)

1
这似乎是与本文的主题无关的哲学切线,但是我认为使用Rails组件确实意味着一个using Rails整体意义。
thelostspore'2


0

使用Descendants_tracker gem可能会有所帮助。以下示例是从gem的文档复制而来的:

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]

抱歉,如果我错过了语法,但是思路应该很清楚(目前我无法访问ruby)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.