首页 文章

从Ruby中的modules / mixins继承类方法

提问于
浏览
78

众所周知,在Ruby中,类方法得到了继承:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

然而,令我惊讶的是它不适用于mixins:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

我知道#extend方法可以做到这一点:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

但我正在编写一个包含实例方法和类方法的mixin(或者,更愿意写):

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

现在我想做的是:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

我想要A,B从 Common 模块继承实例和类方法 . 但是,当然,这不起作用 . 那么,是不是有一个秘密的方法使这个继承从单个模块工作?

我把它分成两个不同的模块似乎不太优雅,一个包含,另一个包括扩展 . 另一种可能的解决方案是使用类 Common 而不是模块 . 但这只是一种解决方法 . (如果有两组常见功能 Common1Common2 并且我们真的需要mixins?怎么办?)有没有深层次的原因为什么类方法继承不能用mixins工作?

4 回答

  • 26

    一个常见的习惯用法是使用 included hook并从那里注入类方法 .

    module Foo
      def self.included base
        base.send :include, InstanceMethods
        base.extend ClassMethods
      end
    
      module InstanceMethods
        def bar1
          'bar1'
        end
      end
    
      module ClassMethods
        def bar2
          'bar2'
        end
      end
    end
    
    class Test
      include Foo
    end
    
    Test.new.bar1 # => "bar1"
    Test.bar2 # => "bar2"
    
  • 150

    以下是完整的故事,解释了理解为什么模块包含在Ruby中的工作方式所需的元编程概念 .

    包含模块时会发生什么?

    将模块包含到类中会将模块添加到类的祖先中 . 您可以通过调用 ancestors 方法查看任何类或模块的祖先:

    module M
      def foo; "foo"; end
    end
    
    class C
      include M
    
      def bar; "bar"; end
    end
    
    C.ancestors
    #=> [C, M, Object, Kernel, BasicObject]
    #       ^ look, it's right here!
    

    当您在 C 的实例上调用方法时,Ruby将查看此祖先列表的每个项目,以便找到具有提供的名称的 instance method . 由于我们将 M 包含在 C 中, M 现在是 C 的祖先,所以当我们在 C 的实例上调用 foo 时,Ruby会在 M 中找到该方法:

    C.new.foo
    #=> "foo"
    

    注意 the inclusion does not copy any instance or class methods to the class - 它只是在类中添加"note",它也应该在包含的模块中查找实例方法 .

    我们模块中的“类”方法怎么样?

    因为包含仅更改实例方法的调度方式,包括将该模块放入该类的类 only makes its instance methods available 中 . 模块中的"class"方法和其他声明不会自动复制到类中:

    module M
      def instance_method
        "foo"
      end
    
      def self.class_method
        "bar"
      end
    end
    
    class C
      include M
    end
    
    M.class_method
    #=> "bar"
    
    C.new.instance_method
    #=> "foo"
    
    C.class_method
    #=> NoMethodError: undefined method `class_method' for C:Class
    

    Ruby如何实现类方法?

    在Ruby中,类和模块是普通对象 - 它们是类 ClassModule 的实例 . 这意味着您可以动态创建新类,将它们分配给变量等:

    klass = Class.new do
      def foo
        "foo"
      end
    end
    #=> #<Class:0x2b613d0>
    
    klass.new.foo
    #=> "foo"
    

    同样在Ruby中,您可以在对象上定义所谓的 singleton methods . 这些方法作为新实例方法添加到对象的特殊隐藏 singleton class

    obj = Object.new
    
    # define singleton method
    def obj.foo
      "foo"
    end
    
    # here is our singleton method, on the singleton class of `obj`:
    obj.singleton_class.instance_methods(false)
    #=> [:foo]
    

    但是类和模块不仅仅是普通的对象吗?事实上他们是!这是否意味着他们也可以使用单例方法?是的,它确实!这就是类方法的诞生方式:

    class Abc
    end
    
    # define singleton method
    def Abc.foo
      "foo"
    end
    
    Abc.singleton_class.instance_methods(false)
    #=> [:foo]
    

    或者,更常见的定义类方法的方法是在类定义块中使用 self ,它引用正在创建的类对象:

    class Abc
      def self.foo
        "foo"
      end
    end
    
    Abc.singleton_class.instance_methods(false)
    #=> [:foo]
    

    如何在模块中包含类方法?

    正如我们刚刚 Build 的那样,类方法实际上只是类对象的singleton类上的实例方法 . 这是否意味着我们可以只添加一堆类方法 include a module into the singleton class ?是的,它确实!

    module M
      def new_instance_method; "hi"; end
    
      module ClassMethods
        def new_class_method; "hello"; end
      end
    end
    
    class HostKlass
      include M
      self.singleton_class.include M::ClassMethods
    end
    
    HostKlass.new_class_method
    #=> "hello"
    

    这个 self.singleton_class.include M::ClassMethods 行看起来不太好,所以Ruby添加了Object#extend,它也是这样做的 - 即包含一个模块到对象的单例类中:

    class HostKlass
      include M
      extend M::ClassMethods
    end
    
    HostKlass.singleton_class.included_modules
    #=> [M::ClassMethods, Kernel]
    #    ^ there it is!
    

    将扩展调用移动到模块中

    前面的示例不是结构良好的代码,原因有两个:

    • 我们现在必须在 HostClass 定义中同时调用 includeextend 才能正确包含我们的模块 . 如果你必须包含许多类似的模块,这可能会变得非常麻烦 .

    • HostClass 直接引用 M::ClassMethods ,这是 M 模块的实现细节, HostClass 不应该知道或关心 .

    那么怎么样:当我们在第一行调用 include 时,我们以某种方式通知模块它已被包含,并且还给它我们的类对象,以便它可以调用 extend 本身 . 这样,它就可以添加类方法,如果它想要的话 .

    这正是 special self.included method 的用途 . 只要模块包含在另一个模块中,Ruby就会自动调用此方法class(或module),并将宿主类对象作为第一个参数传递:

    module M
      def new_instance_method; "hi"; end
    
      def self.included(base)  # `base` is `HostClass` in our case
        base.extend ClassMethods
      end
    
      module ClassMethods
        def new_class_method; "hello"; end
      end
    end
    
    class HostKlass
      include M
    
      def self.existing_class_method; "cool"; end
    end
    
    HostKlass.singleton_class.included_modules
    #=> [M::ClassMethods, Kernel]
    #    ^ still there!
    

    当然,添加类方法并不是我们在_1775700中唯一能做的事情 . 我们有类对象,所以我们可以调用任何其他(类)方法:

    def self.included(base)  # `base` is `HostClass` in our case
      base.existing_class_method
      #=> "cool"
    end
    
  • 2

    正如Sergio在评论中提到的那样,对于已经在Rails中的人(或者不介意依赖于Active Support),Concern在这里很有帮助:

    require 'active_support/concern'
    
    module Common
      extend ActiveSupport::Concern
    
      def instance_method
        puts "instance method here"
      end
    
      class_methods do
        def class_method
          puts "class method here"
        end
      end
    end
    
    class A
      include Common
    end
    
  • 5

    这样做你可以吃蛋糕并吃掉它:

    module M
      def self.included(base)
        base.class_eval do # do anything you would do at class level
          def self.doit #class method
            @@fred = "Flintstone"
            "class method doit called"
          end # class method define
          def doit(str) #instance method
            @@common_var = "all instances"
            @instance_var = str
            "instance method doit called"
          end
          def get_them
            [@@common_var,@instance_var,@@fred]
          end
        end # class_eval
      end # included
    end # module
    
    class F; end
    F.include M
    
    F.doit  # >> "class method doit called"
    a = F.new
    b = F.new
    a.doit("Yo") # "instance method doit called"
    b.doit("Ho") # "instance method doit called"
    a.get_them # >> ["all instances", "Yo", "Flintstone"]
    b.get_them # >> ["all instances", "Ho", "Flintstone"]
    

    如果你打算添加实例和类变量,你最终会拔掉你的头发,因为你会遇到一堆破碎的代码,除非你这样做 .

相关问题