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

Bor*_*cky 89 ruby mixins

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

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works
Run Code Online (Sandbox Code Playgroud)

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

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!
Run Code Online (Sandbox Code Playgroud)

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

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works
Run Code Online (Sandbox Code Playgroud)

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

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end
Run Code Online (Sandbox Code Playgroud)

现在我想做的是:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end
Run Code Online (Sandbox Code Playgroud)

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

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

Ser*_*sev 162

一个常见的习惯用法是included从那里使用hook和inject类方法.

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"
Run Code Online (Sandbox Code Playgroud)

  • `include`添加实例方法,`extend`添加类方法.这是它的工作原理.我没有看到不一致,只有未满足的期望:) (23认同)
  • @BorisStitnicky相信这个答案.这是Ruby中非常常见的习惯用法,正是解决了您所询问的用例以及您遇到的原因.它可能看起来"不优雅",但它是你最好的选择.(如果你经常这样做,你可以将`included`方法定义移动到另一个模块并在主模块中包含THAT;) (6认同)
  • 阅读[此主题](http://www.velocityreviews.com/forums/t864722-why-does-module-include-exclude-the-modules-metaclass.html),了解_"为什么?"_ . (2认同)
  • @werkshy:将模块包含在虚拟类中. (2认同)

Mát*_*osi 36

以下是完整的故事,解释了理解为什么模块包含在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!
Run Code Online (Sandbox Code Playgroud)

当您在实例上调用方法时C,Ruby将查看此祖先列表的每个项目,以便查找具有提供的名称的实例方法.因为我们包括MC,M是现在的祖先C,所以当我们调用foo上的一个实例C,红宝石会发现方法M:

C.new.foo
#=> "foo"
Run Code Online (Sandbox Code Playgroud)

请注意,包含不会将任何实例或类方法复制到类中 - 它只是向类添加"注释",它还应该在包含的模块中查找实例方法.

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

因为列入只是改变了方法实例方法分派,其中包括一个模块在一个类只能使可用的实例方法对类.模块中的"类"方法和其他声明不会自动复制到类中:

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
Run Code Online (Sandbox Code Playgroud)

Ruby如何实现类方法?

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

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

klass.new.foo
#=> "foo"
Run Code Online (Sandbox Code Playgroud)

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

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]
Run Code Online (Sandbox Code Playgroud)

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

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]
Run Code Online (Sandbox Code Playgroud)

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

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]
Run Code Online (Sandbox Code Playgroud)

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

正如我们刚刚建立的那样,类方法实际上只是类对象的singleton类上的实例方法.这是否意味着我们可以在singleton类中包含一个模块来添加一堆类方法?是的,它确实!

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"
Run Code Online (Sandbox Code Playgroud)

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!
Run Code Online (Sandbox Code Playgroud)

extend呼叫转移到模块中

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

  1. 我们现在必须调用两个 includeextendHostClass定义中正确地包含我们的模块.如果你必须包含许多类似的模块,这可能会变得非常麻烦.
  2. HostClass直接引用M::ClassMethods,这是一个实现细节的模块MHostClass不应该需要知道或关心.

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

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

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!
Run Code Online (Sandbox Code Playgroud)

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

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end
Run Code Online (Sandbox Code Playgroud)

  • 精彩的答案!经过一天的努力终于能够理解这个概念了。谢谢。 (2认同)
  • 我认为这可能是我在 SO 上见过的最好的书面答案。感谢您令人难以置信的清晰度并扩展了我对 Ruby 的理解。如果我能给它 100 分的奖金,我会的! (2认同)

Fra*_* Yu 6

正如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
Run Code Online (Sandbox Code Playgroud)