Ruby mixins: расширять и включать
Я читал несколько статей о методах Ruby mixin, extend
и include
, и я все еще не совсем уверен в поведении. Я понимаю, что extend
добавит методы экземпляра данного модуля как однотонные методы к модулю, выполняющему расширение, и что include
по существу добавит содержимое модуля (методы, константы, переменные) к тому, которое делает в том числе, эффективно определяя их в приемнике.
Однако, после некоторого возиться, пытаясь понять, как будет проявляться поведение, у меня есть несколько вопросов. Вот моя настройка тестирования:
module Baz
def blorg
puts 'blorg'
end
end
module Bar
include Baz
def blah
puts 'blah'
end
end
module Foo
extend Bar
end
class Bacon
extend Bar
end
class Egg
include Bar
end
Как и следовало ожидать, модуль Bar
получает методы экземпляра, определенные в Baz
(#blorg
), как если бы они были определены сами по себе из-за метода включения, а класс Bacon
методы Bacon::blah
и Bacon::blorg
по расширению.
Bacon.blah # => blah
Bacon.blorg # => blorg
И класс Egg
получает методы, определенные в Bar
(#blah
и теперь #blorg
) как методы экземпляра.
Egg.new.blah # => blah
Egg.new.blorg # => blorg
Я получаю все это, так что хорошо.
Однако я не понимаю ответов, получаемых от методов #ancestors
и #is_a?
.
Bacon.ancestors # => [Bacon, Object, Kernel, BasicObject]
Bacon.is_a? Bar # => true
Egg.ancestors # => [Egg, Bar, Baz, Object, Kernel, BasicObject]
Egg.is_a? Bar # => false
Казалось бы, расширение модуля заставляет метод #is_a?
возвращать true
при запросе об этом модуле, но он не добавляется к предкам класса, и наоборот в отношении включения: предки класс содержит включенные модули, но метод #is_a?
возвращает false
при запросе. Почему это происходит?
Ответы
Ответ 1
Разница в том, что include
добавит включенный класс к предкам класса включения, тогда как extend
добавит расширенный класс к предкам класса Singleton класса расширяющихся классов. Уф. Пусть сначала наблюдаем, что происходит:
Bacon.ancestors
#=> [Bacon, Object, Kernel, BasicObject]
Bacon.singleton_class.ancestors
#=> [Bar, Baz, Class, Module, Object, Kernel, BasicObject]
Bacon.new.singleton_class.ancestors
#=> [Bacon, Object, Kernel, BasicObject]
Bacon.is_a? Bar
#=> true
Bacon.new.is_a? Bar
#=> false
И для класса Egg
Egg.ancestors
#=> [Egg, Bar, Baz, Object, Kernel, BasicObject]
Egg.singleton_class.ancestors
#=> [Class, Module, Object, Kernel, BasicObject]
Egg.new.singleton_class.ancestors
#=> [Egg, Bar, Baz, Object, Kernel, BasicObject]
Egg.is_a? Bar
#=> false
Egg.new.is_a? Bar
#=> true
Итак, что действительно делает foo.is_a? Klass
, это проверить, содержит ли foo.singleton_class.ancestors
Klass
. Другое дело, что все предки класса становятся предками одноуровневого класса экземпляров при создании экземпляра. Таким образом, это будет оцениваться как true для всех вновь созданных экземпляров любого класса:
Egg.ancestors == Egg.new.singleton_class.ancestors
Итак, что все это значит? extend
и include
делают то же самое на разных уровнях, я надеюсь, что следующий пример делает это понятным, поскольку оба способа расширения класса по существу эквивалентны:
module A
def foobar
puts 'foobar'
end
end
class B
extend A
end
class C
class << self
include A
end
end
B.singleton_class.ancestors == C.singleton_class.ancestors
#=> true
где class << self
- это просто нечетный синтаксис, чтобы перейти к одиночному классу. Таким образом, extend
действительно является сокращением для include
в одноэлементном классе.
Ответ 2
Egg.is_a? Egg # => false
Включает (эффективно) меняет экземпляры класса Egg
. Хотя это не совсем то же самое, он очень похож на что-то вроде
class Egg < Bar
end
Когда расширение добавит методы класса, так что это очень похоже на выполнение чего-то вроде
class Bacon
class << self
include Bar
end
end
Вы можете думать об этом, например, включать экземпляры изменений класса, где в качестве расширения фактически изменяется класс.