Примечания переводчика: Всем привет. Данная статья представляет собой вольный перевод (ссылка в конце).
Я не претендую на 100% правильность перевода.
Однако я думаю, что полностью передал общую суть происходящего.
Кому может быть полезна эта статья? Скорее всего, для начинающих разработчиков Ruby on Rails, которым просто интересно понять некоторые аспекты работы Ruby. Для кого эта статья может быть бесполезна? Скорее всего, для чистокровных программистов Ruby и опытных разработчиков Ruby on Rails. Высока вероятность, что вы это уже знаете.
Зачем я сделал перевод? Эта статья показалась мне интересной, и у меня просто внутри возникло желание поделиться ею со всем русскоязычным (возможно, плохо английским) сообществом.
P.S. Если вы знаете английский, просто перейдите по ссылке в конце.
Ниже приводится текст перевода: На днях я спрашивал всех, знает ли кто-нибудь хорошее и краткое объяснение объектов Ruby и системы диспетчеризации методов.
И ответ некоторых людей был: «Нет, вам следует об этом написать».
Итак, вот статья.
Я собираюсь объяснить, как работает объектная система Ruby, включая поиск методов, наследование, суперклассы, классы, примеси и одноэлементные методы.
Мое понимание пришло не из чтения источников MRI, а из повторной реализации этой системы, один раз на JavaScript и один раз на Ruby. Если вы хотите прочитать небольшую, но почти правильную реализацию, то это хорошее место для начала.
В связи с тем, что я на самом деле не читал исходники, в этой статье будет объяснено, что происходит в Ruby с логической точки зрения, а не с точки зрения того, что происходит на самом деле.
Это просто модель, которая поможет вам понять некоторые вещи.
Начнем с самого начала.
Вы можете создать объектную систему Ruby почти полностью из одних только модулей.
Думайте о модулях как о наборе методов.
Например, модуль A содержит методы foo и bar.
Когда вы пишете def foo. end внутри модуля Ruby, вы добавляете этот метод в модуль, вот и все.+----------+ | module A | +----------+ | def foo | | def bar | +----------+
Модуль может иметь несколько родителей.
Когда вы пишете: module B
include A
end
Все, что вам нужно сделать, это добавить A в качестве родительского элемента к B. Никакие методы не копируются, мы просто создаем указатель от B к A. +-----------+
| module A |
+-----------+
| def foo |
| def bar |
+-----------+
^
|
+-----+-----+
| module B |
+-----------+
| def hello |
| def bye |
+-----------+
Модуль может иметь несколько родителей, образуя тем самым дерево.
Например, эти модули: module A
def foo ; end
def bar ; end
end
module B
def hello ; end
def bye ; end
end
module C
include B
def start ; end
def stop ; end
end
module D
include A
include C
end
Они формируют это дерево в соответствии с порядком их включения.
+-----------+
| module B |
+-----------+
| def hello |
| def bye |
+-----------+
^
+-----------+ +-----+-----+
| module A | | module C |
+-----------+ +-----------+
| def foo | | def start |
| def bar | | def stop |
+-----------+ +-----------+
^ ^
+-------------------+-------------------+
|
+-----+-----+
| module D |
+-----------+
Важный момент, который объяснит, как Ruby находит местоположение определения метода, — это «родословная» моделей («родословная» модуля).
Вы можете попросить модуль предоставить вам свое «предок», и он предоставит его вам в виде массива модулей: >> D.ancestors
=> [D, C, B, A]
Важно то, что это генеалогическое древо в виде простой цепочки, а не в виде дерева.
Эта цепочка определяет порядок, в котором мы перебираем модули, чтобы найти метод. Чтобы составить этот список, мы начнем с D и углубимся, проходя всех родителей справа налево.
Поэтому порядок включения вызовов очень важен.
Родительские элементы модуля расположены по порядку, и это определяет порядок, в котором они будут искаться.
Когда мы хотим найти, где определен метод, мы проходим по цепочке наследования, пока не найдем первый модуль, в котором он определен.
Если ни один из модулей не содержит этот метод, мы ищем снова, но на этот раз ищем метод с именем Method_missing. Если ни один из модулей не содержит метода, выдается исключение NoMethodError. Цепочка наследования модулей решает проблему, когда два модуля содержат один и тот же метод. Будет вызван метод, модуль которого стоит первым в цепочке наследования.
Мы можем использовать возможности Ruby, чтобы определить, чей метод использовался при его вызове.
>> D.instance_method(:foo)
=> #<UnboundMethod: D(A)#foo>
>> D.instance_method(:hello)
=> #<UnboundMethod: D(B)#hello>
>> D.instance_method(:start)
=> #<UnboundMethod: D(C)#start>
UnboundMethod — это просто представление метода модели до его привязки к объекту.
Когда вы видите D(A)#foo, это означает, что D унаследовал метод #foo от A. Если вы вызываете #foo для объекта, который включает D, вы получите метод, определенный в A. Говоря об объектах, почему мы еще не сделали ни одного? Какая польза от набора методов, если нет объекта, к которому мы можем его применить.
Что ж, именно здесь в игру вступает Класс.
В Ruby класс — это подкласс модуля, что звучит странно, но помните, что все они представляют собой структуры данных, хранящие методы.
Класс почти похож на модуль: он хранит методы и может содержать другие модули, но у него есть некоторые дополнительные возможности.
Одним из которых является возможность создавать объекты.
class K
include D
end
k = K.new
У нас снова есть возможность определить, откуда берутся методы объекта.
>> k.method(:start)
=> #<Method: K(C)#start>
Это показывает, что когда мы вызываем k.start, мы получаем метод #start из модуля C. Вы заметите, что когда вы вызываете instance_method модуля, он возвращает UnboundMethod, а в случае объекта Method. Разница в том, что метод связан с объектом.
Когда вы вызываете #call для объекта, поведение такое же, как и при использовании k.start. UnboundMethods нельзя вызвать напрямую, поскольку у них нет объекта, который мог бы их вызвать.
Это выглядит так: мы ищем метод, начиная с класса, которому принадлежит объект, затем просматриваем всю цепочку наследования, пока не найдем, где определен метод. Что ж, это почти правда, но у Ruby есть еще одна хитрость: одноэлементные методы.
Вы можете добавлять новые методы к объекту и только к этому объекту, не добавляя его в класс.
Видеть: >> def k.mart ; end
>> k.method(:mart)
=> #<Method: #<K:0x00000001f78248>.
mart>
Мы также можем добавлять их в модули, потому что.
Модули — это всего лишь один тип объектов.
>> def B.roll ; end
>> B.method(:roll)
=> #<Method: B.roll>
Если имя метода содержит (.
) вместо хеша (#), это означает, что метод существует только для этого объекта, а не находится в модуле.
Однако ранее мы говорили, что Ruby использует модули для хранения методов; простые старые объекты не имели такой возможности.
Так где же хранятся одноэлементные методы? Каждый объект в Ruby (помните, что модули и классы также являются объектами) имеет так называемые метаклассы, также известные как одноэлементные классы, собственные классы или виртуальные классы.
Задача этих классов — просто хранить методы одноэлементных объектов.
Изначально они не содержат никаких методов и имеют единственный родительский класс — объектный класс.
Итак, для нашего объекта k цепочка наследования будет выглядеть так: +-----------+
| module B |
+-----------+
^
+-----------+ +-----+-----+
| module A | | module C |
+-----------+ +-----------+
^ ^
+-------------------+-------------------+
|
+-----+-----+
| module D |
+-----------+
^
+-----+-----+
| class K |
+-----------+
^
+-----+-----+ +---+
| metaclass |<~~~~~~~~+ k |
+-----------+ +---+
Мы можем попросить Ruby показать метакласс объекта.
Здесь мы видим, что метакласс — это анонимный класс, привязанный к объекту k, и у него есть метод экземпляра #mart, которого нет в классе K. >> k.singleton_class
=> #<Class:#<K:0x00000001f78248>>
>> k.singleton_class.instance_method(:mart)
=> #<UnboundMethod: #<Class:#<K:0x00000001f78248>>#mart>
>> K.instance_method(:mart)
NameError: undefined method `mart' for class `K'
Один момент, на который стоит обратить внимание: метакласс не фигурирует в цепочке наследования, но следует понимать, что он все равно участвует в цепочке поиска места, где определен метод.
Когда мы вызываем метод объекта k, объект спрашивает свой метакласс, содержит ли он этот метод, а затем метакласс проходит по цепочке наследования, чтобы определить, где находится метод. Методы Singleton находятся в метаклассе и имеют приоритет над методами, определенными в объектном классе и всех его родительских элементах.
Теперь мы подошли ко второму особому свойству класса, помимо его способности создавать объекты.
Классы имеют особую форму наследования, называемую подклассами.
Каждый класс имеет один и только один суперкласс, по умолчанию — Object. С точки зрения вызова метода суперклассы можно рассматривать как первый родительский модуль класса: class Foo < Bar class Foo
include Extras =~ include Bar
end include Extras
end
Итак, цепочка наследования дает нам [Foo, Extras, Bar] в обоих случаях и она, как и прежде, определяет порядок поиска методов.
(На самом деле это выглядит как [Foo, Extras, Bar, Object, Kernel, BasicObject], но мы рассмотрим это через минуту.
) Обратите внимание, что Ruby нарушает принцип подстановки Лискова, не позволяя включать классы; таким образом можно использовать только модули, а не их подтипы.
Показанный выше фрагмент показывает, как создание подклассов влияет на порядок поиска метода, а код справа не будет работать, если Bar является классом.
Если создание подклассов — это то же самое, что включение, зачем нам нужны обе эти функции? Что ж, это дает нам еще одну возможность.
Классы наследуют одноэлементные методы своих суперклассов, но в случае модулей этого не происходит. module Z
def self.z ; :z ; end
end
class Bar
def self.bar ; :bar ; end
end
class Foo < Bar
include Z
end
# Singleton methods from Bar work on Foo .
>> Bar.bar => :bar >> Foo.bar => :bar # .
but singleton methods from Z don't
>> Z.z
=> :z
>> Foo.z
NoMethodError: undefined method `z' for Foo:Class
Мы можем смоделировать это с точки зрения родительских отношений, сказав, что метаклассы подклассов имеют метаклассы суперкласса в качестве своих родителей.
+-----+ +--------------+
| Bar +~~~~~~~~>| #<Class:Bar> |
+-----+ +--------------+
^ ^
| |
+--+--+ +-------+------+
| Foo +~~~~~~~~>| #<Class:Foo> |
+-----+ +--------------+
Действительно, если мы посмотрим на Foo, мы увидим, что его метод #bar происходит из метакласса Bar. >> Foo.method(:bar)
=> #<Method: Foo(Bar).
bar>
>> Foo.singleton_class.instance_method(:bar)
=> #<UnboundMethod: #<Class:Bar>#bar>
Мы увидели, как порядок наследования и поиска методов в Ruby можно изобразить в виде дерева модулей, при этом создание подклассов и подклассов создают различные родительские отношения.
Мы также объяснили одиночное и множественное наследование методов объекта и одноэлементных методов.
Теперь давайте посмотрим на несколько вещей, которые есть на задней части этой модели.
Первый — это метод Object#extend. Вызывая object.extend(M), мы делаем методы из модуля M доступными в объекте.
Мы не копируем методы, мы просто добавляем M в качестве родителя метакласса этого объекта.
Если объект имеет класс Thing, мы получаем следующее отношение: +-------+ +-----+
| Thing | | M |
+-------+ +-----+
^ ^
+-------+-----+
|
+--------+ +---------+-------+
| object +~~~~~~~~>| #<Class:object> |
+--------+ +-----------------+
Таким образом, расширение объекта модулем равносильно включению этого модуля в метакласс объекта.
(На самом деле есть некоторая разница, но это не относится к данной теме).
Глядя на это дерево, мы видим, что когда мы вызываем метод объекта, система диспетчеризации методов отдает предпочтение методам, определенным в модуле M, методам в Thing (будет использоваться метод из M), и, в свою очередь, методы найденный в метаклассе объекта, будет иметь приоритет над M и Thing. Этот контекст важен: мы не можем сказать, что методы в M имеют приоритет над Thing в общем смысле, а только в том случае, когда мы говорим о вызове метода объекта.
Важна цепочка наследования, в которой ищется метод. И это становится очевидным, когда мы изучаем, как работает супер.
Взгляните на следующий набор модулей: module X
def call ; [:x] ; end
end
module Y
def call ; super + [:y] ; end
end
class Test
include X
include Y
end
Цепочка наследования для Test — [Test, Y, X], поэтому, если мы вызываем Test.new.call, мы вызываем метод #call для Y. Но что происходит, когда Y вызывает super? У Y нет собственной цепочки наследования, то есть нет никого, к кому Y мог бы вызвать этот метод, верно?
Но нет. Когда мы сталкиваемся с вызовом super, важно то, что мы вызвали метод в цепочке наследования объекта, вот и все.
Вы можете думать о поиске метода как о поиске всех определений данного метода в цепочке наследования метакласса объекта.
>> t = Test.new
>> t.singleton_class.ancestors.map { |m|
m.instance_methods(false).
include?(:call) ? m.instance_method(:call) : nil }.
compact
=> [#<UnboundMethod: Y#call>, #<UnboundMethod: X#call>]
Чтобы определить местоположение метода, мы вызываем первый метод в цепочке наследования.
Если этот метод вызывает super, мы переходим к следующему и так далее, пока не закончим поиск.
Если бы Test не включал модуль X, не было бы реализации #call, кроме той, которая определена в Y, поэтому вызов super привел бы к ошибке.
Действительно, в нашем случае Test.new.call вернет [:x, :y].
Мы почти закончили, но я обещал рассказать вам, что такое Object, Kernel и BasicObject. BasicObject — корневой класс всей системы; это класс без суперкласса.
Object наследует от BasicObject и является базовым суперклассом для всех пользовательских классов.
Разница между ними в том, что у BasicObject почти нет методов, а у Object довольно много: методы ядра Ruby, такие как: #==, #__send__, #dup, #inspect, #instance_eval, #is_a?, #method, #respond_to? и #to_s. Хотя на самом деле сам Object не содержит всех этих методов, он получает их от Ядра.
Ядро — это просто модуль с набором всех объектных методов ядра Ruby. Итак, если мы попытаемся отобразить объектную систему ядра Ruby, мы получим следующее: +---------------+ +------------+
| | | |
| +-----------+----------+ +-------------+ +--------+ +--------+--------+ |
| | #<Class:BasicObject> |<~~~~+ BasicObject | | Kernel +~~~~>| #<Class:Kernel> | |
| +----------------------+ +-------------+ +--------+ +-----------------+ |
| ^ ^ ^ |
| | +-------+--------+ |
| | | |
| +--------+--------+ +----+---+ |
| | #<Class:Object> |<~~~~~~~~~~~~~~~~+ Object | |
| +-----------------+ +--------+ |
| ^ ^ |
| | | |
| +--------+--------+ +----+---+ |
| | #<Class:Module> |<~~~~~~~~~~~~~~~~+ Module |<-----------------------------------+
| +-----------------+ +--------+
| ^ ^
| | |
| +--------+--------+ +----+---+
| | #<Class:Class> |<~~~~~~~~~~~~~~~~+ Class |
| +-----------------+ +--------+
| ^
| |
+-----------------------------------------------+
На этой диаграмме показаны модули и классы ядра Ruby: BasicObject, Kernel, Object, Module и Class, их метаклассы и то, как они все связаны.
Да, BasicObject.singleton_class.superclass — это класс.
Руби творит немного магии вуду, чтобы заставить шарманку работать (примечание переводчика).
В любом случае, если вы хотите понять диспетчеризацию методов в Ruby, просто запомните: Модуль — это набор методов.
Модуль может иметь много родителей Класс — это модуль, который может создавать объекты У каждого объекта есть метакласс, родительским элементом которого является класс объекта.
Создание подклассов означает соединение двух классов и их метаклассов.
Методы находятся путем погружения в «генеалогическое древо», просматривая ветви справа налево.
Нет, я не знаю всех тонкостей этой работы.
Никто не знает. Оригинальная статья: blog.jcoglan.com/2013/05/08/how-ruby-method-dispatch-works Теги: #ruby на рельсах #ruby #ruby #ruby на рельсах
-
Адамс, Джон Коуч
19 Oct, 24 -
Подводные Камни — Анимация Травы (Unity3D)
19 Oct, 24 -
Арифметика 2.0?
19 Oct, 24 -
В Яндекс.метрике Появилось Много «Целей»
19 Oct, 24 -
Представляем Jms 2.0
19 Oct, 24