【问题标题】:Ruby mixin - multiple inheritanceRuby mixin - 多重继承
【发布时间】:2017-05-28 22:32:32
【问题描述】:

我最近学习了如何通过模块实现继承。但是,如果我想实现多重继承,我看不到干净的解决方案。 这是使用单继承并调用超类的初始化方法的原始示例。:

module ModuleB  
  def initialize
    puts "initialize from ModuleB"
    @b = 5
  end
end

module ModuleA
  include ModuleB

  def initialize
    super
    puts "initialize from ModuleA"
    @a = @b
  end

  def action_1
    @a = @b + 1
  end
end

class ClassA
  include ModuleA
  def initialize
    super
    puts 'initialize - method in ClassA'
    @c = @a
    @d = @b
    puts "a = #@a"
    puts "b = #@b"
    puts "c = #@c"
    puts "d = #@d"
  end

end

instA = ClassA.new
puts instA.action_1

但是,如果 ClassA 中包含额外的 ModuleC,我们将如何调用“超类”初始化方法?

    module ModuleB  
      def initialize
        puts "initialize from ModuleB"
        @b = 5
      end
    end

    module ModuleA
      include ModuleB

      def initialize
        super
        puts "initialize from ModuleA"
        @a = @b
      end

      def action_1
        @a = @b+1
        @a = @b + 1
      end
    end

    module ModuleC
      include ModuleB

      def initialize
        super
        puts "initialize from ModuleC"
        @a = 'cute Zuzia'
      end

      def action_1
        @a = 'something'
      end
    end


    class ClassA
      include ModuleA
      include ModuleC
      def initialize
        super  # which initialize will be called? From moduleA or from moduleC?
                # what if I wanted to invoke ModuleC initialize from classA instance as well?
        puts 'initialize - method in ClassA'
        @c = @a
        @d = @b
        puts "a = #@a"
        puts "b = #@b"
        puts "c = #@c"
        puts "d = #@d"
      end

    end

在这种情况下如何使用mixin启用多重继承?

【问题讨论】:

  • 我知道这段代码“只是为了好玩”,但我认为您曾经不想在 module 中定义 initialize 方法- 那只是自找麻烦。同样,我会害怕任何在模块之间混合方法的代码......所以虽然你是正确的,语言的行为有些不清楚(就像任何复杂的继承示例一样,在任何语言中!),这是一个非常您正在试验的不切实际的代码示例。

标签: ruby inheritance mixins


【解决方案1】:

将调用哪个初始化?来自 moduleA 还是来自 moduleC?

class C
  include M
end

非常简单:它使M 成为C 的超类,而之前的C 超类成为M 的超类。换句话说,它只不过是沼泽标准无聊的旧类继承。 (更准确地说:它创建了一个与M 共享其方法表指针、常量表指针和类变量表指针的类,并使该类成为C 的超类和C 的前超类超类这个新创建的类。它还会检查M 是否已经在C 的祖先链中,在这种情况下它什么也不做。)

module M1
  include M2
end

显然,这不能使M2 成为M1 的超类,因为模块没有超类。它所做的只是简单地记录M2M1 的祖先这一事实,以便将来当M1included 进入一个类并为M1 创建一个类时, M2 将包含在这个新创建的类中。

所以,这一切都只是类继承。简单、无聊的类继承。

让我们看看你的代码中的情况:

module ModuleB; end

好的,所以我们定义了一个名为ModuleB的模块。

module ModuleA
  include ModuleB
end

我们定义了一个名为ModuleA 的模块,它内部的某个地方存储了这样一个事实:当它把included 放入一个类时,ModuleB 也应该是included。 (让我们假设有一个实例变量@__included_modules__ 或类似的东西,而Module#include 只是实现为@__included_modules__ << m)。

module ModuleC
  include ModuleB
end

这里我们再次定义了一个名为ModuleC 的模块,它内部的某个地方存储了这样一个事实:当它将included 放入一个类时,ModuleB 也应该是included。

class ClassA
  include ModuleA
  include ModuleC
end

让我们一步一步来。

  1. ClassA 的超类是 Object(如果没有明确指定,则它是隐式超类)。
  2. include ModuleA 首先检查ModuleA 是否已经在ClassA 的祖先链中。不是。
  3. 因此,它创建了一个新类(我们称之为〚ModuleA′〛)。
  4. 它使ObjectClassA 的当前超类)成为〚ModuleA′〛 的超类
  5. 它使〚ModuleA′〛 成为ClassA 的超类。此时,祖先链如下所示:ClassA〚ModuleA′〛ObjectKernelBasicObject
  6. 现在它只是按照记录顺序为ModuleA 中记录的每个模块重复所有内容。
  7. 首先,Ruby 检查ModuleB 是否已经在ClassA 的祖先链中。不是。
  8. Ruby 创建了一个类,我们称之为〚ModuleB′〛
  9. Ruby 使〚ModuleA′〛 的当前超类,当然Object 成为〚ModuleB′〛 的超类。
  10. 它使〚ModuleB′〛 成为〚ModuleA′〛 的超类。此时,祖先链如下所示:ClassA〚ModuleA′〛〚ModuleB′〛ObjectKernelBasicObject
  11. 现在我们已经完成了include ModuleA 行,然后转到include ModuleC。同样,这都是相同的简单步骤:
  12. include ModuleC 首先检查 ModuleC 是否已经在 ClassA 的祖先链中。不是。
  13. 因此,它创建了一个新类(我们称之为〚ModuleC′〛)。
  14. 它使〚ModuleA′〛ClassA 的当前超类)成为〚ModuleC′〛 的超类
  15. 它使〚ModuleC′〛 成为ClassA 的超类。此时,祖先链如下所示:ClassA〚ModuleC′〛〚ModuleA′〛〚ModuleB′〛ObjectKernelBasicObject
  16. 现在它只是按照记录的顺序重复 ModuleC 中记录的每个模块的所有内容。
  17. 首先,Ruby 检查ModuleB 是否已经在ClassA 的祖先链中。这是。所以它不会再得到included。
  18. 我们完成了!

ClassA 的祖先链如下所示:ClassA〚ModuleC′〛〚ModuleA′〛〚ModuleB′〛ObjectKernelBasicObject

所以,重复你的问题:

将调用哪个初始化?来自 moduleA 还是来自 moduleC?

super 只是将祖先链“提升了一步”。因为我们在ClassA#initializesuper 将沿着祖先链向上,从ClassA 的超类开始,直到找到另一个同名的方法。所以,它将从〚ModuleC′〛 开始,它确实找到了initialize 方法并运行它。

如果我也想从classA 实例调用ModuleC#initialize 怎么办?

嗯,这就是我们刚刚所做的,不是吗?只需致电super

或者你的意思是没有也调用ClassA#initialize?在这种情况下,您可以做的是直接获取ModuleC#initialize并将其绑定到instA

meth = ModuleC.instance_method(:initialize)
bound_meth = meth.bind(instA)
bound_meth.()

我知道很多 Ruby 教程都在大肆展示所有这些听起来很复杂。但事实并非如此。 include 使包含的模块成为它所包含的任何内容的超类。时期。 真的很简单。 Mixin 继承只是继承。其他一切,方法查找,常量查找,super,不管怎样,都一样。

【讨论】:

    【解决方案2】:

    要回答“将调用哪个初始化?从模块A 还是从模块C?”的问题,它是moduleintialize 和最后一个include。这是 Ruby 中的一般规则。

    你的例子确实让我思考了一下,即使你这样做只是为了好玩。对于如何实现你想要的,我有一个稍微不同的想法。

    1) 创建一个与 modules 通用的模块,该模块定义了 procs 来完成您的 initialize 方法的工作。这里的一个缺点是我正在改变常量。

    module C
        INIT = []
    end
    
    module A
        include C
    
        INIT << ->{puts "initialize() of Module A"}
    end
    
    module B
        include C
    
        INIT << ->{puts "initialize() of Module B"}
    end
    
    class D
        include A
        include B
    
        def initialize
            # Note that you don't need the 'C::' part here,
            # but it is safer.
            C::INIT.each(&:call)
    
            puts "initialize of class D"
        end
    end
    
    d = D.new
    

    2.) 与上面类似,但避免使用常量。尽管如此,还是使用procs。

    module C
        @@initializers = []
        def add_initializer(proc)
            @@initializers << proc
        end
    
        def initializers
            @@initializers
        end
    end
    
    module A
        extend C
    
        add_initializer(->{puts "initialize() of Module A"})
    end
    
    module B
        extend C
    
        add_initializer(->{puts "initialize() of Module B"})
    end
    
    class D
        include A
        include B
    
        include C
    
        def initialize
            initializers.each(&:call)
    
            puts "initialize of class D"
        end
    end
    
    d = D.new
    

    顺便说一句,“使用procs 注册代码以便稍后执行”模式在 Rails 中非常突出。

    编辑:

    如果您想做一些比在proc 的主体中添加puts 更复杂的事情,您可以改为在proc 的主体中调用一个方法来完成您想要的繁重工作。

    我已将module A的定义更改如下:

    module A
        extend C
    
        class << self
            def initializer
                puts "initialize() of Module A"         
            end
        end
    
        add_initializer(->{initializer})
    end
    

    【讨论】:

    • 您的两种解决方案似乎都很有趣。但是,如果有比单个 puts 'abc' 更复杂的代码,add_initializer 调用会是什么样子?可以将包含此类代码的方法名称传递给add_initializer 吗?这样的代码会是什么样子?
    • 好点。您可以在proc 的主体内添加方法调用。这种方法可以完成您需要的繁重工作。我在答案中添加了 EDIT 部分,并在其中添加了此示例。
    猜你喜欢
    • 2014-10-11
    • 2012-05-02
    • 2010-10-23
    • 1970-01-01
    • 2013-02-20
    • 1970-01-01
    • 2017-04-21
    • 1970-01-01
    • 2010-11-08
    相关资源
    最近更新 更多