Cari semua keturunan kelas di Ruby

144

Saya dapat dengan mudah naik hierarki kelas di Ruby:

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

Apakah ada cara untuk menurunkan hierarki juga? Saya ingin melakukan ini

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

tapi sepertinya tidak ada descendantsmetode.

(Pertanyaan ini muncul karena saya ingin menemukan semua model dalam aplikasi Rails yang turun dari kelas dasar dan mencantumkannya; Saya memiliki pengontrol yang dapat bekerja dengan model seperti itu dan saya ingin dapat menambahkan model baru tanpa harus memodifikasi controller.)

Douglas Squirrel
sumber

Jawaban:

146

Berikut ini sebuah contoh:

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

menempatkan Parent.descendants memberi Anda:

GrandChild
Child

menempatkan Child.descendants memberi Anda:

GrandChild
Petros
sumber
1
Itu berhasil, terima kasih! Saya kira mengunjungi setiap kelas mungkin terlalu lambat jika Anda mencoba mencukur milidetik, tetapi sangat cepat bagi saya.
Douglas Squirrel
1
singleton_classalih-alih Classmembuatnya lebih cepat (lihat sumber di apidock.com/rails/Class/descendants )
brauliobo
21
Hati-hati jika Anda mungkin memiliki situasi di mana kelas belum dimuat dalam memori, ObjectSpacetidak akan memilikinya.
Edmund Lee
Bagaimana saya bisa membuat ini bekerja untuk Objectdan BasicObject?, Penasaran ingin tahu apa yang mereka tunjukkan
Amol Pujari
@AmolPujari p ObjectSpace.each_object(Class)akan mencetak semua kelas. Anda juga bisa mendapatkan turunan dari kelas apa pun yang Anda inginkan dengan mengganti namanya selfpada baris kode dalam metode ini.
BobRodes
62

Jika Anda menggunakan Rails> = 3, Anda memiliki dua opsi. Gunakan .descendantsjika Anda ingin lebih dari satu tingkat kedalaman kelas anak-anak, atau gunakan .subclassesuntuk tingkat pertama kelas anak-anak.

Contoh:

class Animal
end

class Mammal < Animal
end

class Dog < Mammal
end

class Fish < Animal
end

Animal.subclasses #=> [Mammal, Fish] 
Animal.descendants  #=> [Dog, Mammal, Fish]
dgilperez
sumber
6
Perhatikan bahwa dalam pengembangan, jika Anda ingin mematikan pemuatan, metode ini hanya akan mengembalikan kelas jika telah dimuat (yaitu jika mereka sudah dirujuk oleh server yang sedang berjalan).
stephen.hanson
1
@ stephen.hanson apa cara paling aman untuk menjamin hasil yang benar di sini?
Chris Edwards
26

Ruby 1.9 (atau 1.8.7) dengan iterators berantai bagus:

#!/usr/bin/env ruby1.9

class Class
  def descendants
    ObjectSpace.each_object(::Class).select {|klass| klass < self }
  end
end

Ruby pra-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

Gunakan seperti ini:

#!/usr/bin/env ruby

p Animal.descendants
Jörg W Mittag
sumber
3
Ini juga berfungsi untuk Modul; ganti saja kedua instance "Class" dengan "Module" dalam kode.
korinthe
2
Untuk keamanan tambahan, seseorang harus menulis ObjectSpace.each_object(::Class)- ini akan menjaga kode berfungsi ketika Anda memiliki YourModule :: Kelas didefinisikan.
Rene Saarsoo
19

Mengganti metode kelas dengan nama inherited . Metode ini akan melewati subkelas ketika dibuat yang dapat Anda lacak.

Chandra Sekar
sumber
Saya suka yang ini juga. Mengganti metode sedikit mengganggu, tetapi membuat metode turunan sedikit lebih efisien karena Anda tidak harus mengunjungi setiap kelas.
Douglas Squirrel
@ Douglas Meskipun kurang mengganggu, Anda mungkin harus bereksperimen untuk melihat apakah memenuhi kebutuhan Anda (yaitu kapan Rails membangun hirarki controller / model?).
Josh Lee
Ini juga lebih portabel untuk berbagai implementasi ruby ​​non-MRI, beberapa di antaranya memiliki overhead kinerja yang serius dari penggunaan ObjectSpace. Warisan # kelas ideal untuk menerapkan pola "pendaftaran otomatis" di Ruby.
John Whitley
Mau berbagi contoh? Karena ini level kelas, saya kira Anda harus menyimpan setiap kelas dalam semacam variabel global?
Noz
@NoT Tidak, variabel instan pada kelas itu sendiri. Tapi kemudian benda-benda itu tidak bisa dikumpulkan oleh GC.
Phrogz
13

Atau (diperbarui untuk ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Cara yang kompatibel dengan Ruby 1.8:

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

Perhatikan bahwa ini tidak akan berfungsi untuk modul. Juga, YourRootClass akan disertakan dalam jawaban. Anda dapat menggunakan Array # - atau cara lain untuk menghapusnya.

apeiros
sumber
itu luar biasa. Bisakah Anda jelaskan bagaimana cara kerjanya? Saya menggunakanObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West
1
Di ruby ​​1.8, class<<some_obj;self;endmengembalikan singleton_class objek. Di 1.9+ Anda bisa menggunakan some_obj.singleton_class(memperbarui jawaban saya untuk mencerminkan itu). Setiap objek adalah turunan dari singleton_class, yang berlaku untuk kelas juga. Karena Each_object (SomeClass) mengembalikan semua instance SomeClass, dan SomeClass adalah turunan dari SomeClass.singleton_class, Each_object (SomeClass.singleton_class) akan mengembalikan SomeClass dan semua subclass.
apeiros
10

Meskipun menggunakan karya ObjectSpace, metode kelas yang diwariskan tampaknya lebih cocok di sini di bawah (Ruby subclass) dokumentasi

Objectspace pada dasarnya adalah cara untuk mengakses apa saja dan segala sesuatu yang saat ini menggunakan memori yang dialokasikan, jadi iterasi setiap elemen untuk memeriksa apakah itu adalah subkelas dari kelas Animal yang tidak ideal.

Dalam kode di bawah ini, metode kelas Hewan yang diwariskan mengimplementasikan panggilan balik yang akan menambahkan subkelas yang baru dibuat ke array turunannya.

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

  def self.descendants
    puts @descendants 
  end
end
Mateo Sossah
sumber
6
`@descendants || = []` jika tidak, Anda hanya akan mendapatkan keturunan terakhir
Axel Tetzlaff
4

Saya tahu Anda bertanya bagaimana melakukan ini dalam warisan tetapi Anda dapat mencapai ini dengan langsung di Ruby dengan spasi nama kelas ( Classatau 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]

Ini jauh lebih cepat dengan cara ini dibandingkan dengan membandingkan setiap kelas ObjectSpaceseperti yang diusulkan solusi lain.

Jika Anda benar-benar membutuhkan ini dalam warisan, Anda dapat melakukan sesuatu seperti ini:

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

tolok ukur di sini: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

memperbarui

pada akhirnya yang harus Anda lakukan adalah ini

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

terima kasih @ saturnflyer atas sarannya

setara8
sumber
3

(Rails <= 3.0) Atau Anda dapat menggunakan ActiveSupport :: DescendantsTracker untuk melakukan akta. Dari sumber:

Modul ini menyediakan implementasi internal untuk melacak keturunan yang lebih cepat daripada iterasi melalui ObjectSpace.

Karena modularisasinya bagus, Anda bisa 'memilih-ceri' modul tertentu untuk aplikasi Ruby Anda.

chtrinh
sumber
2

Ruby Facets memiliki keturunan Class #,

require 'facets/class/descendants'

Ini juga mendukung parameter jarak generasi.

trans
sumber
2

Versi sederhana yang memberikan array semua keturunan kelas:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
Dorian
sumber
1
Ini sepertinya jawaban yang unggul. Sayangnya itu masih menjadi mangsa pemuatan kelas malas. Tapi saya pikir mereka semua melakukannya.
Dave Morse
@ DaveMorse Saya akhirnya mendaftarkan file dan secara manual memuat konstanta agar mereka terdaftar sebagai keturunan (dan kemudian akhirnya menghapus semua ini: D)
Dorian
1
Perhatikan bahwa #subclassesini dari Rails ActiveSupport.
Alex D
1

Anda dapat require 'active_support/core_ext'dan menggunakan descendantsmetode ini. Periksa dokumen , dan coba IRB atau coba-coba. Dapat digunakan tanpa Rel.

thelostspore
sumber
1
Jika Anda harus menambahkan dukungan aktif ke Gemfile Anda, maka itu tidak benar-benar "tanpa rel". Hanya memilih potongan-potongan rel yang Anda suka.
Caleb
1
Ini tampak seperti garis singgung filosofis yang tidak relevan dengan topik di sini, tetapi saya pikir bahwa menggunakan komponen Rails tidak selalu berarti seseorang using Railsdalam arti holistik.
thelostspore
0

Rails menyediakan metode subclass untuk setiap objek, tetapi tidak didokumentasikan dengan baik, dan saya tidak tahu di mana itu didefinisikan. Ini mengembalikan array nama kelas sebagai string.

Daniel Tsadok
sumber
0

Menggunakan permata descendants_tracker dapat membantu. Contoh berikut disalin dari dokumen permata:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Permata ini digunakan oleh permata virtus populer , jadi saya pikir ini cukup solid.

EricC
sumber
0

Metode ini akan mengembalikan hash multidimensi dari semua keturunan Obyek.

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

{ MasterClass => descendants_mapper(MasterClass) }
jozwright
sumber
-1

Jika Anda memiliki akses ke kode sebelum subkelas dimuat, Anda dapat menggunakan metode yang diwarisi .

Jika Anda tidak (yang bukan kasus tetapi mungkin berguna bagi siapa saja yang menemukan posting ini) Anda bisa menulis:

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

Maaf jika saya melewatkan sintaks tetapi ide harus jelas (saya tidak memiliki akses ke ruby ​​saat ini).

Maciej Piechotka
sumber