ค้นหาผู้สืบทอดทั้งหมดของคลาสใน 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

ทำให้ Parent.descendants ให้คุณ:

GrandChild
Child

ทำให้ Child.descendants ให้คุณ:

GrandChild

1
มันใช้งานได้ดีขอบคุณ! ฉันคิดว่าการไปทุกคลาสอาจช้าเกินไปถ้าคุณพยายามโกนหนวดเป็นมิลลิวินาที แต่มันก็เร็วสำหรับฉันอย่างสมบูรณ์
Douglas Squirrel

1
singleton_classแทนที่จะClassทำให้เร็วขึ้น (ดูแหล่งที่apidock.com/rails/Class/descendants )
brauliobo

21
ระวังถ้าคุณอาจมีสถานการณ์ที่คลาสไม่ได้โหลดในหน่วยความจำยังObjectSpaceไม่ได้
Edmund Lee

ฉันจะทำให้งานนี้ObjectและBasicObjectอยากรู้อยากเห็นที่จะรู้ว่าสิ่งที่พวกเขาปรากฏขึ้นมา?
Amol Pujari

@AmolPujari p ObjectSpace.each_object(Class)จะพิมพ์คลาสทั้งหมดออก นอกจากนี้คุณยังสามารถรับลูกหลานของคลาสใด ๆ ที่คุณต้องการโดยการ substutiting ชื่อสำหรับ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

1
@ stephen.hanson วิธีที่ปลอดภัยที่สุดในการรับประกันผลลัพธ์ที่ถูกต้องคืออะไร
Chris Edwards

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

3
งานนี้สำหรับโมดูลเกินไป เพียงแทนที่ "Class" ทั้งสองด้วย "Module" ในรหัส
korinthe

2
เพื่อความปลอดภัยเป็นพิเศษควรเขียนไว้ObjectSpace.each_object(::Class)- สิ่งนี้จะทำให้รหัสทำงานเมื่อคุณมี YourModule :: Class ที่กำหนดไว้
Rene Saarsoo

19

แทนที่วิธีการเรียนที่มีชื่อได้รับมรดก เมธอดนี้จะถูกส่งผ่านคลาสย่อยเมื่อสร้างขึ้นซึ่งคุณสามารถติดตามได้


ฉันชอบอันนี้เหมือนกัน การเอาชนะวิธีนี้เป็นการล่วงล้ำเล็กน้อย แต่มันทำให้วิธีการสืบทอดนั้นมีประสิทธิภาพเพิ่มขึ้นเล็กน้อยเนื่องจากคุณไม่ต้องไปทุกชั้น
Douglas Squirrel

@ ดักลาสแม้ว่ามันจะรบกวนน้อยคุณอาจต้องทดลองดูว่ามันตรงกับความต้องการของคุณหรือไม่ (เช่นเมื่อไหร่ Rails จะสร้างลำดับชั้นของตัวควบคุม / โมเดล?)
Josh Lee

นอกจากนี้ยังสามารถพกพาได้ง่ายขึ้นกับการใช้งานทับทิมที่ไม่ใช่ MRI ซึ่งบางตัวมีค่าใช้จ่ายประสิทธิภาพสูงจากการใช้ ObjectSpace Class # ที่สืบทอดนั้นเหมาะสำหรับการนำรูปแบบ "การลงทะเบียนอัตโนมัติ" ไปใช้ใน Ruby
John Whitley

ต้องการแบ่งปันตัวอย่างหรือไม่ เนื่องจากเป็นระดับชั้นฉันเดาว่าคุณจะต้องเก็บแต่ละคลาสไว้ในตัวแปรโกลบอล
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

10

แม้ว่าการใช้งาน ObjectSpace ที่วิธีการเรียนที่สืบทอดมาดูเหมือนว่าจะดีขึ้นเหมาะกับที่นี่ได้รับการถ่ายทอด (subclass) เอกสารทับทิม

Objectspace นั้นเป็นวิธีเข้าถึงทุกอย่างและทุกอย่างที่ใช้หน่วยความจำที่จัดสรรอยู่ในปัจจุบันดังนั้นการวนซ้ำทุกองค์ประกอบเพื่อตรวจสอบว่าเป็นคลาสย่อยของคลาสสัตว์หรือไม่

ในโค้ดด้านล่างเมธอด Animal คลาสที่สืบทอดมาจะใช้การเรียกกลับที่จะเพิ่มคลาสย่อยที่สร้างขึ้นใหม่ไปยังอาเรย์ลูกหลานของมัน

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

  def self.descendants
    puts @descendants 
  end
end

6
`@descendants || = []` ไม่เช่นนั้นคุณจะได้ลูกหลานคนสุดท้ายเท่านั้น
Axel Tetzlaff

4

ฉันรู้ว่าคุณกำลังถามวิธีการทำสิ่งนี้ในการสืบทอด แต่คุณสามารถทำสิ่งนี้ได้โดยตรงใน Ruby โดยการเว้นวรรคชื่อ ( Classหรือ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]

วิธีนี้เร็วกว่าการเปรียบเทียบกับทุกชั้นเรียน 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]

ขอบคุณ @surnurnflyer สำหรับข้อเสนอแนะ


3

(Rails <= 3.0) หรือคุณสามารถใช้ ActiveSupport :: DescendantsTracker เพื่อทำโฉนด จากแหล่งที่มา:

โมดูลนี้จัดให้มีการติดตั้งภายในเพื่อติดตามการสืบทอดซึ่งเร็วกว่าการวนซ้ำผ่าน ObjectSpace

เนื่องจากมันได้รับการทำให้เป็นโมดูลอย่างสวยงามคุณจึงสามารถ 'เชอร์รี่เลือก' โมดูลนั้นสำหรับแอพ Ruby ของคุณ


2

Ruby Facets มีลำดับชั้น #

require 'facets/class/descendants'

นอกจากนี้ยังสนับสนุนพารามิเตอร์ระยะ generational


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 หรือแงะ สามารถใช้งานได้โดยไม่มีราง


1
หากคุณต้องเพิ่มการรองรับที่แอคทีฟใน Gemfile ของคุณนั่นไม่ใช่ "ไม่มีทางรถไฟ" มันแค่เลือกชิ้นส่วนของรางที่คุณชอบ
Caleb

1
ดูเหมือนว่าการสัมผัสกันทางปรัชญาที่ไม่เกี่ยวข้องกับหัวข้อที่นี่ แต่ฉันคิดว่าการใช้ส่วนประกอบ Rails นั้นจำเป็นต้องหมายความว่าusing Railsมีความหมายแบบองค์รวม
thelostspore

0

Rails มีวิธีการ subclasses สำหรับทุกวัตถุ แต่มันไม่ได้รับการบันทึกไว้อย่างดีและฉันไม่รู้ว่ามันถูกกำหนดไว้ที่ไหน ส่งคืนอาร์เรย์ของชื่อคลาสเป็นสตริง


0

การใช้descendant_tracker gemอาจช่วยได้ ตัวอย่างต่อไปนี้ถูกคัดลอกจาก doc ของ gem:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

อัญมณีนี้ถูกใช้โดยได้รับความนิยมอัญมณีตัสดังนั้นฉันคิดว่ามันเป็นของแข็งสวย


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.