为包含的方法调用 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 而不是模块的实例方法的原因。
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 之间进入继承链。
这样做,因为然后
此 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 而不是模块的实例方法的原因。