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

puts Parent.descendantsはあなたに与えます:

GrandChild
Child

置くChild.descendantsはあなたに与える:

GrandChild

1
ありがとうございます。ミリ秒を削ろうとすると、すべてのクラスを訪問するのが遅すぎるかもしれませんが、私にとっては完全に高速です。
ダグラスリス

1
singleton_classClassより速くする代わりに(apidock.com/rails/Class/descendantsのソースを参照してください
brauliobo

21
クラスがまだメモリにロードされていない場合は、それを取得できないことに注意してくださいObjectSpace
Edmund Lee

どのように私はこれを機能させることができますか?ObjectそしてBasicObject、彼らが何が現れるかを知りたいと思っています
Amol Pujari 2017

@AmolPujari p ObjectSpace.each_object(Class)はすべてのクラスを出力します。selfメソッドのコード行でクラスの名前を置き換えることにより、必要なクラスの子孫を取得することもできます。
BobRodes

62

Rails> = 3を使用する場合、2つのオプションがあります。使用.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 2018年

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
これはモジュールでも機能します。コード内の「Class」の両方のインスタンスを「Module」に置き換えるだけです。
コリンテス

2
安全性をObjectSpace.each_object(::Class)高めるために、次のように記述する必要があります。これにより、YourModule :: Classが定義された場合でもコードが機能し続けます。
Rene Saarsoo、2011年

19

inheritedという名前のクラスメソッドをオーバーライドます。このメソッドは、作成時に追跡できるサブクラスに渡されます。


私もこれが好きです。メソッドのオーバーライドは少し煩わしいですが、すべてのクラスにアクセスする必要がないので、子孫メソッドが少し効率的になります。
ダグラスリス

@Douglas煩わしさは少ないですが、ニーズを満たしているかどうかを確認するために実験する必要があります(つまり、Railsはいつコントローラー/モデル階層を構築するのですか?)。
ジョシュリー

また、さまざまな非MRIルビー実装への移植性が高く、その一部にはObjectSpaceの使用による深刻なパフォーマンスオーバーヘッドがあります。Class#inheritedは、Rubyで「自動登録」パターンを実装するのに最適です。
John Whitley

例を共有したいですか?それはクラスレベルなので、各クラスを何らかのグローバル変数に格納する必要があると思いますか?
Noz、2014

@Nozいいえ、クラス自体のインスタンス変数です。ただし、オブジェクトはGCで収集できません。
Phrogz 2015

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

私はあなたが継承でこれを行う方法を尋ねていることを知っていますが、クラス(ClassまたはModule)に名前空間を付けることで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ファセットにはClass#descendantsがあり、

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にアクティブサポートを追加する必要がある場合、それは実際には「レールなし」ではありません。好きなレールを選ぶだけです。
カレブ

1
これは、ここでのトピックとは関係のない哲学的な正接のように見えますが、Railsコンポーネントを使用することはusing Rails、必ずしも全体論的な意味があることを意味すると思います。
thelostspore 2015

0

Railsはすべてのオブジェクトにサブクラスメソッドを提供しますが、十分に文書化されておらず、どこに定義されているのかわかりません。クラス名の配列を文字列として返します。


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]

構文を忘れた場合は申し訳ありませんが、アイデアは明確になっているはずです(現時点ではルビにアクセスできません)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.