为什么实例上的扩展方法在实例上的继承方式不同?

时间:2019-05-05 10:50:30

标签: ruby oop inheritance

module B
  def a
    print 'B'
    super
  end
end

class A
  extend B

  def a
    print "A"
  end

  def self.a
    print "A"
  end
end

a = A.new

a.extend B
puts a.a # => BA
puts A.a # => A

为什么Kernel#extend方法对类对象和类实例对象的工作方式不同?如果我们扩展一个实例,它看起来像是在继承链中加了模块,但是如果我们扩展了一个类,它将模块放在类之上。

2 个答案:

答案 0 :(得分:3)

首先让我介绍一些概念。

首先,使用def self.a定义类方法与在类的单例类上定义方法相同:

class C
  def self.a; end

  class << self
    def b; end
  end
end

C.method(:a) # => #<Method: C.a>
C.method(:b) # => #<Method: C.b>

此外,对象上的方法是该对象单例类上的实例方法:

C.singleton_class.instance_method(:a) # => #<UnboundMethod: #<Class:C>#a>
C.singleton_class.instance_method(:b) # => #<UnboundMethod: #<Class:C>#b>

如果您查看上面如何定义#b,您会发现我们没有以self为前缀,因此这只是一个实例方法。

接下来,#extend与单例类上的#include相同:

module M; end

class C1
  extend M
end

class C2
  class << self
    include M
  end
end

C1.ancestors # => [#<Class:C2>, M, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
C2.ancestors # => [#<Class:C1>, M, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

请注意,M现在如何以相同的方式成为C1C2的祖先的一部分。

也可以通过以下方式实现对M的包含(或扩展):

C1.extend M
C2.singleton_class.include M

最后,请注意当我们#include一个模块时祖先会发生什么:

module M1; end
module M2; end
class C; end

C.include M1
C.ancestors # => [C, M1, Object, Kernel, BasicObject]

C.include M2
C.ancestors # => [C, M2, M1, Object, Kernel, BasicObject]

每个#include都导致模块插入到祖先链中的接收方(在这种情况下为C)之后。

现在让我们来看一下您的定义(省略主体):

module B; end

class A
  extend B
end

请记住,#extend#include上的#singleton_class相同。因此,我们可以将其重写如下:

module B; end
class A; end
A.singleton_class.include B

单例类的祖先现在在第一项之后有B,这是A的单例类,其中定义了类方法(请记住,类方法因此只是单例上的实例方法有关课程的课程):

A.singleton_class.ancestors # => [#<Class:A>, B, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

继续进行代码的第二部分:

a = A.new
a.extend B

使用#include重写它:

a = A.new
a.singleton_class.include B

让我们检查一下祖先:

a.singleton_class.ancestors # => [#<Class:#<A:0x00007f83e714be88>>, B, A, Object, Kernel, BasicObject]

同样,#include将模块放置在祖先链中的第一个元素之后,导致BA之前。

这意味着将#a发送到a(即a.a)时,它将寻找响应#a的第一个祖先,即{{1} } 在这种情况下。 B然后将调用B,它将沿始祖链继续,在此链中会找到对super做出响应的A

现在#a会有所不同。记住A.a的单例课程的祖先:

A

请注意,A.singleton_class.ancestors # => [#<Class:A>, B, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject] 之后是B#<Class:A>已经响应#<Class:A>,这是#a上的类方法。由于该方法不会调用A,因此将永远不会调用super。因此,您不会获得相同的输出。

如果您想在B#a之前加入B,则必须在B之前加入#<Class:A>的单身人士课。 A在祖先链的最开始插入一个对象,与#prepend不同,后者将其插入到第一项之后(您必须删除代码中的#include才能起作用,否则就没有作用如果extend B已经是祖先,则发生这种情况):

B

现在调用A.singleton_class.prepend B A.singleton_class.ancestors # => [B, #<Class:A>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject] 将产生与A.a相同的结果,即打印a.a

答案 1 :(得分:0)

A.singleton_class.ancestors
   #=> [#<Class:A>, B, #<Class:Object>, #<Class:BasicObject>, Class, Module,
   #    Object, Kernel, BasicObject] 
A.method(:a).owner
   #=> #<Class:A> 

所以我们不要为此感到惊讶

A.a 

打印"A",而不打印"B"

现在让我们考虑在B的实例上扩展A

aa = A.new
aa.extend B

aas = aa.singleton_class
  #=> #<Class:#<A:0x00005b19441bf6a8>> 
aas.methods.include?(:a)
  #=> true
aa.method(:a).owner
  #=> B 
aas.superclass
  #=> A
m = aa.method(:a).super_method
  #=> #<Method: A#a> 
m.owner
  #=> A 

因此

aa.a
BA

首先调用在a的单例类中定义的方法aa,该方法打印字母"B",然后调用其超类A来执行{{ 1}},该方法为A.a,导致其打印A::a