为包含的方法调用 super 会导致 "no superclass method" 错误 - ActiveSupport

Calling super for included method results in "no superclass method" error - ActiveSupport

Ruby 中有一个“super”关键字,它正在查看祖先链,以便找到链上的第一个方法实现并执行它。所以,这就是它在 Ruby 中的工作方式,不足为奇:

module Mammal
  def walk
    puts "I'm walking"
  end
end
require '~/Documents/rubytest/super/mammal.rb'

class Cat
  include Mammal

  def walk
    super
  end
end
2.7.0 :001 > simba = Cat.new
2.7.0 :002 > simba.walk
I'm walking
 => nil

这是理想的行为。现在,在 Rails 中有 ActiveSupport::Concern 为模块提供了一些额外的功能。如果您使用 ActiveSupport 助手以某种类似的方式进行操作,会发生以下情况:

module MammalMixin
  extend ActiveSupport::Concern
    
  included do
    def show
      @mammal = Mammal.find(params[:id])
    end
  end
end
class SomeController < ApplicationController
  include MammalMixin

  def show
    super
  end
end

如果你到达那个控制器,这将出错: super: #SomeController:0x000055f07c549bc0

没有超类方法“show”

当然,可以不使用“included do”助手并恢复为普通的 Ruby 风格,但是有人可以建议 ActiveSupport::Concern 中究竟是什么阻止了“super”正常工作并且(也许)解释这背后的理由?

我一直在查看 active_support/concern.rb 中的源代码,但未能理解。

这里的问题是 included 做的事情与您的第一个示例不同,它是由 ActiveSupport::Concern 定义的自定义挂钩,允许您编写 class 宏(例如将 scope 添加到 ActiveModel)。在引擎盖下这是包含的内容:

  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      <your-block-executes-here>
    end
  end

因此,当将一个块传递给 included 时,该块将直接在 class 上进行评估,并且没有 super 可供参考;您实际上是在覆盖方法,而不是覆盖

答案在the documentation of ActiveSupport::Concern#included[粗体强调我的]:

Evaluate given block in context of base class, so that you can write class macros here.

所以,这是你的块的内容:

def show
 @mammal = Mammal.find(params[:id])
end

并且根据文档,此块在基础 class 的上下文中被计算 。现在,当您在 class 的上下文中评估方法 def 初始化表达式时会发生什么?您在 class!

中定义了一个方法

所以,你在这里做的是在 SomeController class 中定义一个名为 show 的方法,就像你写的一样:

class SomeController < ApplicationController
  def show
    @mammal = Mammal.find(params[:id])
  end

  def show
    super
  end
end

换句话说,你的第二个定义是overwriting the first definition, not overriding it,所以没有超级方法。

使用ActiveSupport::Concern#included的正确方法是这样的:

module MammalMixin
  extend ActiveSupport::Concern

  def show
    @mammal = Mammal.find(params[:id])
  end
    
  included do
    acts_as_whatever
  end
end

ActiveSupport::Concern#included,如文档所述,用于执行代码(例如“class macros” like acts_as_*, has_many, belongs_to,等)在 class.

的上下文中

:

写的时候

class C
  include M
end

您调用的是Module#include method (which is not overriden by Class,因此继承不变。

现在,Module#include 实际上并没有做任何有趣的事情。它基本上看起来像这样:

class Module
  def include(mod)
    mod.append_features(self)
  end
end

这是一个 classic Double Dispatch 习惯用法,让模块完全控制它希望如何包含在 class 中。当正在呼叫

C.include(M)

这意味着 C 处于控制之中,它只是委托给

M.append_features(C)

这让 M 处于控制之中。

Module#append_features通常所做的,是下面的(我将在伪Ruby中描述它,因为行为无法在Ruby, 因为必要的数据结构在引擎内部):

class Module
  def append_features(base)
    if base.is_a?(Module)
      base.included_modules << self unless base.included_modules.include?(self)
    else
      old_superclass = base.__superclass__

      klazz = Class.new(old_superclass)
      klazz.__constant_table__ = __constant_table__
      klazz.__class_variable_table__ = __class_variable_table__
      klazz.__instance_variable_table__ = __instance_variable_table__
      klazz.__method_table__ = __method_table__
      klazz.__virtual__ = true

      base.__superclass__ = klazz
    end

    included(base)

    self
  end
end

所以,Ruby 创建了一个新的 class,称为 include class,其常量 table指针,class变量table指针,实例变量table指针,方法table指针指向常量table,class变量table、实例变量table、模块的方法table。基本上,我们正在创建一个 class 来隐藏模块。

然后它使这个class成为class的新超级class,并使旧超级class成为包含的超级class class。实际上,它 inserts include class 在 class 和 superclass 之间进入继承链。

这样做,因为然后:去class,检查方法是否存在,如果不fetch superclass,检查方法是否存在, 等等等等。由于方法查找是面向对象语言的执行引擎中最常见和最重要的操作之一,因此算法简单快速至关重要。

include class 将被 Class#superclass method, so you don't see it, but it will be displayed by Module#ancestors.

跳过

这就是 super 工作的原因:因为模块 字面上 变成了超级 class.

我们以 C < Object 开始,以 C < M' < Object 结束。

现在,ActiveSupport::Concern 完全搞砸了。

ActiveSupport::Concern#included method 的有趣部分是:

@_included_block = block

它只是存储块供以后使用。

正如我上面所解释的,当 MammalMixin 被包含到 SomeController 中时,即当 SomeController.include(MammalMixin) 被调用时,SomeController.include(即 Module#include)将依次调用 MammalMixin.append_features(SomeController)MammalMixin.append_features 在这种情况下是 ActiveSupport::Concern#append_features,最有趣的部分是:

base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)

如您所见,它使用 Module#class_eval 来评估它先前在包含它的基础 class 的上下文中保存的块。这就是使您的方法最终成为基础 class 而不是模块的实例方法的原因。