Почему метод расширения класса экземпляр работает с наследованием по-разному? - PullRequest
1 голос
/ 05 мая 2019
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 # extension работает по-разному для объектов класса и объекта экземпляра класса? Похоже, что он добавляет модуль в цепочку наследования, если мы расширяем экземпляр, но помещает модуль выше класса, если мы расширяем класс.

Ответы [ 2 ]

3 голосов
/ 05 мая 2019

Позвольте мне сначала представить несколько понятий.

Во-первых, определение метода класса с использованием def self.a аналогично определению метода в классе singleton-класса:

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 теперь тоже относится к предкам C1 и C2.

Включение (или расширение с) 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 поместил модуль после первого элемента в цепочке предков, что привело к B перед A.

Это означает, что при отправке #a на a (т.е. a.a), он будет искать первого предка, который отвечает на #a, который в данном случае равен 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 до #<Class:A>, вам нужно добавить B к классу синглтона 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]

Invoking A.a теперь будет производить то же, что и a.a, а именно print BA.

0 голосов
/ 06 мая 2019
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 для выполнения A.a, этот метод является A::a , заставляя его печатать "A".

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...