【问题标题】:Look up all descendants of a class in Ruby在 Ruby 中查找一个类的所有后代
【发布时间】:2011-01-24 12:31:03
【问题描述】:

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

【问题讨论】:

    标签: ruby


    【解决方案1】:

    这是一个例子:

    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
    

    puts Child.descendants 给你:

    GrandChild
    

    【讨论】:

    • 效果很好,谢谢!我想如果你想减少毫秒,访问每节课可能会太慢,但对我来说非常快。
    • singleton_class 而不是 Class 使它更快(请参阅apidock.com/rails/Class/descendants 的源代码)
    • 如果您可能遇到类尚未加载到内存中的情况,请注意ObjectSpace不会有它。
    • 我怎样才能让 ObjectBasicObject 工作?,很想知道他们出现了什么
    • @AmolPujari p ObjectSpace.each_object(Class) 将打印出所有类。您还可以通过在方法的代码行中将其名称替换为 self 来获取所需的任何类的后代。
    【解决方案2】:

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

    【讨论】:

    • 请注意,在开发过程中,如果您关闭了预先加载,这些方法将仅返回已加载的类(即,如果它们已被正在运行的服务器引用)。
    • @stephen.hanson 在这里保证正确结果的最安全方法是什么?
    • 在开发模式下,在调用.subclasses.descendants之前使用require_dependency自动加载任何必要的文件。要获取所需的路径,请使用路径或 glob,例如 Rails.root.glob('pattern/to/*/file_*.rb)。它甚至可以在初始化程序中完成,如这里的答案所述:stackoverflow.com/questions/29662518/…
    【解决方案3】:

    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
    

    【讨论】:

    • 这也适用于模块;只需在代码中用“模块”替换“类”的两个实例即可。
    • 为了额外的安全,应该写ObjectSpace.each_object(::Class) - 当你碰巧定义了 YourModule::Class 时,这将使代码保持工作。
    【解决方案4】:

    重写名为inherited 的类方法。此方法将在创建时传递给您可以跟踪的子类。

    【讨论】:

    • 我也喜欢这个。覆盖该方法有点侵入性,但它使后代方法更有效率,因为您不必访问每个类。
    • @Douglas 虽然它的侵入性较小,但您可能需要进行试验以查看它是否满足您的需求(即 Rails 何时构建控制器/模型层次结构?)。
    • 它也更易于移植到各种非 MRI 的 ruby​​ 实现中,其中一些因使用 ObjectSpace 而产生了严重的性能开销。 Class#inherited 非常适合在 Ruby 中实现“自动注册”模式。
    • 愿意分享一个例子吗?由于它是类级别,我猜您必须将每个类存储在某种全局变量中?
    • @Noz 不,类本身的实例变量。但是GC无法收集对象。
    【解决方案5】:

    或者(为 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&lt;&lt;MyClass;self;end) {|it| puts it}
    • 在 ruby​​ 1.8 中,class&lt;&lt;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 及其所有子类。
    【解决方案6】:

    虽然使用ObjectSpace可以,但是继承类的方法似乎更适合这里inherited(subclass) Ruby documentation

    对象空间本质上是一种访问当前正在使用分配内存的任何事物的方法,因此迭代其每个元素以检查它是否是 Animal 类的子类并不理想。

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

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

    【讨论】:

    • ` @descendants ||= []` 否则你只会得到最后一个后代
    【解决方案7】:

    我知道你在问如何在继承中做到这一点,但你可以通过命名空间类(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 的建议

    【讨论】:

      【解决方案8】:

      (Rails

      这个模块提供了一个内部实现来跟踪后代,这比遍历 ObjectSpace 更快。

      由于它很好地模块化,您可以为您的 Ruby 应用程序“挑选”特定模块。

      【讨论】:

        【解决方案9】:

        一个简单的版本,给出一个类的所有后代的数组:

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

        【讨论】:

        • 这看起来是一个很好的答案。不幸的是,它仍然成为延迟加载类的牺牲品。但我认为他们都这样做。
        • @DaveMorse 我最终列出了文件并手动加载常量以将它们注册为后代(然后最终删除了整个事情:D)
        • 请注意,#subclasses 来自 Rails ActiveSupport。
        【解决方案10】:

        Ruby Facets 有 Class#descendants,

        require 'facets/class/descendants'
        

        它还支持世代距离参数。

        【讨论】:

          【解决方案11】:

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

          【讨论】:

            【解决方案12】:

            您可以require 'active_support/core_ext' 并使用descendants 方法。 Check out the doc,然后在 IRB 中试一试或撬动它。可以在没有 Rails 的情况下使用。

            【讨论】:

            • 如果你必须为你的 Gemfile 添加主动支持,那么它并不是真正的“没有 Rails”。它只是选择你喜欢的轨道。
            • 这似乎是与这里的主题无关的哲学切线,但我认为使用 Rails 组件确实意味着一个整体意义上的using Rails
            【解决方案13】:

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

            如果您在rails console 中闲逛,您只需命名该类,它就会被加载。从那时起,它将显示在subclasses

            在某些情况下,您可能需要在代码中强制加载类。单表继承 (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]
            

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

            是的,这里有各种各样的代码气味。接受或离开它......它允许您在开发中离开急切的加载,同时仍然进行动态类操作。一旦投入生产,这对性能没有影响。

            【讨论】:

              【解决方案14】:

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

              class Foo
                extend DescendantsTracker
              end
              
              class Bar < Foo
              end
              
              Foo.descendants # => [Bar]
              

              这个 gem 被流行的virtus gem 使用,所以我认为它很可靠。

              【讨论】:

                【解决方案15】:

                此方法将返回一个对象所有后代的多维散列。

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

                【讨论】:

                  【解决方案16】:

                  计算任意类的传递壳

                  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
                  

                  【讨论】:

                    【解决方案17】:

                    类#descendants (Ruby 3.1+)

                    从 Ruby 3.1 开始,Class#descendants 是一个内置方法。

                    它返回一个类的所有后代,不包括接收者和单例类。

                    因此,不再需要依赖 ActiveSupport 或编写猴子补丁来使用它。

                    class A; end
                    class B < A; end
                    class C < B; end
                    
                    A.descendants    #=> [B, C]
                    B.descendants    #=> [C]
                    C.descendants    #=> []
                    

                    来源:

                    【讨论】:

                      【解决方案18】:

                      如果您在加载任何子类之前可以访问代码,那么您可以使用inherited 方法。

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

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

                      对不起,如果我错过了语法,但想法应该很清楚(我目前无法访问 ruby​​)。

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2021-10-16
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多