从Ruby中的模块/混合继承类方法


95

众所周知,在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我们真的需要有混入?)有什么深层原因类方法的继承不会混入工作?



1
有了这样的区别,我知道这是有可能的-我在要求这样做的最丑陋方式,以及为什么天真的选择不起作用的原因。
鲍里斯·斯蒂尼克

1
凭着更多的经验,我知道如果包含一个模块并将模块方法添加到includer的singleton类中,那么Ruby猜测程序员的意图就太过分了。这是因为“模块方法”实际上只是单例方法。模块不是专门用于具有单例方法的,它们特别是用于定义方法和常量的名称空间。名称空间与模块的单例方法完全无关,因此,实际上,单例方法的类继承比模块中缺少类的更令人惊讶。
鲍里斯·史提尼克

Answers:


171

一个常见的习惯用法是included从那里使用钩子和注入类方法。

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"

26
include添加实例方法,extend添加类方法。这就是它的工作方式。我没有看到不一致的地方,只有未满足的期望:)
塞尔吉奥·图伦采夫

1
我慢慢接受这样一个事实,即您的建议与解决该问题的实际解决方案一样优雅。但是,我很想知道为什么与类一起使用的东西不能与模块一起工作的原因。
鲍里斯·斯蒂尼克

6
@BorisStitnicky相信这个答案。这是Ruby中非常常见的习惯用法,它可以精确地解决您所问的用例,并可以精确地解决您遇到的原因。它可能看起来“毫无意义”,但这是您最好的选择。(如果经常执行此操作,则可以将included方法定义移至另一个模块,并在主模块中包含THAT;)
Phrogz 2012年

2
阅读此主题以获取有关“为什么”的更多见解
Phrogz 2012年

2
@werkshy:将模块包含在虚拟类中。
塞尔吉奥·图伦采夫2015年

47

这是完整的故事,解释了必要的元编程概念,这些概念理解了为什么模块包含在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会查看此祖先列表的每一项,以查找具有提供的名称的实例方法。由于我们包含M在中CM现在是的祖先C,因此当我们调用foo的实例时C,Ruby会在M以下位置找到该方法:

C.new.foo
#=> "foo"

请注意,包含不会将任何实例或类方法复制到该类 –只是在类上添加“注释”,它也应该在包含的模块中查找实例方法。

那么我们模块中的“类”方法呢?

因为包含仅更改实例方法的分配方式,所以将模块包含到类中只会使其实例方法在该类上可用。模块中的“类”方法和其他声明不会自动复制到该类:

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中,类和模块是纯对象-它们是类Class和的实例Module。这意味着您可以动态创建新类,将它们分配给变量,等等:

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

klass.new.foo
#=> "foo"

同样在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]

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

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]

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

正如我们刚刚建立的那样,类方法实际上只是类对象的单例类上的实例方法。这是否意味着我们可以仅将模块包含在单例类中以添加大量类方法?是的,它确实!

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,它的作用相同-即在对象的singleton类中包含一个模块:

class HostKlass
  include M
  extend M::ClassMethods
end

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

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!

当然,添加类方法并不是我们唯一能做的self.included。我们有类对象,因此可以在其上调用任何其他(类)方法:

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

2
很棒的答案!经过一天的努力,终于能够理解这个概念。谢谢。
Sankalp

1
我认为这可能是我在SO上见过的最好的书面答案。感谢您令人难以置信的清晰度,并扩展了我对Ruby的理解。如果我可以为此赠予100pt的奖金,我会的!
Peter Nixey

7

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

3

您可以这样做来吃蛋糕,也可以吃:

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

如果打算添加实例和类变量,除非您这样做,否则最终将陷入困境,因为您将遇到一堆损坏的代码。


在传递class_eval块时,有些奇怪的事情不起作用,例如定义常量,定义嵌套类以及在方法外部使用类变量。为了支持这些东西,你可以给一个class_eval定界符(串),而不是块:base.class_eval << - “END”
保罗·多诺霍
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.