Позвольте мне сначала представить несколько понятий.
Во-первых, определение метода класса с использованием 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
.