Данный опус является моей попыткой собрать воедино все принципы, согласно которым работает объектная модель в языке Ruby. Цель статьи в том, чтобы читатель увидел логику там, где раньше видел “магию”.
Первый раздел предназначен скорее для тех, кто с Ruby знаком слабо; практикующим рубистам следует его пропустить и переходить к описанию вызова метода.
Основы
Ruby является полностью объектно-ориентированным языком: числа, строки, регулярные выражения, массивы - это все объекты определенных классов. Класс определяет поведение объекта - он содержит все методы, доступные его объектам (инстансам).
1 2 3 4 5 6 7 8 |
|
Традиционно в ООП используется наследование классов - методы класса-родителя доступны объектам дочерних классов (а также их дочерних классов итд). Разумеется, в ruby этот механизм тоже не забыт:
1 2 3 4 5 6 7 8 9 10 11 |
|
Однако, множественное наследование не поддерживается: у каждого класса может быть только один родительский класс - за исключением BasicObject
(в 1.8 и ранее Object
), который является корнем иерархии и не имеет родителя. В случаях, когда нужно предоставить какое-то кол-во методов нескольким классам, но нежелательно пользоваться наследованием (скажем, когда нужно наследоваться от какого-то другого класса), удобно использовать механизм примесей (mixins). Это происходит следующим образом: в модуле определяются методы, потом определенный класс подключает этот модуль - и объекты класса получают все методы, определенные в модуле. При этом кол-во включаемых в класс модулей ограничено только здравым смыслом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Вызов метода
Итак, каждый объект является инстансом определенного класса, а класс в свою очередь имеет цепочку наследования, которая восходит вверх до BasicObject
.
Объект содержит какое-то количество инстанс-переменных (например, объект класса Person
может иметь инстанс-переменную @name
) и ссылку на свой класс.
Класс содержит определенное количество инстанс-методов (так я буду называть методы, которые класс предоставляет своим объектам и объектам дочерних классов, чтобы не путать их с классовыми методами - такими как Time.now
), некоторое число подключенных модулей и ссылку на родительский класс.
Посмотрим, что происходит, когда вызывается определенный метод:
- определяется объект-получатель вызова
- в иерархии классов ищется сам метод
Начнем с получателя. В любом участке кода всегда есть текущий объект - его возвращает псевдопеременная self
. У текущего объекта есть два основных значения: при обращении к инстанс-переменным ruby будет искать их в текущем объекте, и при вызове метода без указания получателя он будет вызван на текущем объекте. При вызове метода с указанием получателя, соответственно, получателем будет указанный объект.
Так вот. Как только выполнение попадает внутрь вызванного метода, self
становится этим объектом-получателем и остается им до тех пор, пока не будет вызван метод с другим объектом-получателем, либо пока метод не будет полностью выполнен (а также в некоторых других случаях, которые будут описаны далее). Все это время обращения к инстанс-переменным будут работать с текущим объектом, и вызовы методов без указания получателя будут попадать также в текущий объект.
Теперь посмотрим на поиск метода в иерархии классов. У нас уже есть объект-получатель вызова, а он содержит ссылку на свой класс. Если класс содержит метод с таким названием, то этот метод используется; иначе поиск продолжается в классе-родителе, потом в его родителе итд - вплоть до BasicObject
.
Если же метод так и не был найден, будет вызван метод method_missing
с параметрами, первым из которых будет название вызванного метода, далее аргументы, переданные при вызове, и блок при его наличии. Вызов происходит по той же схеме - сначала method_missing
ищется в классе объекта, потом в его родителе итд. Стандартный BasicObject#method_missing
выбрасывает NoMethodError
, если вызов добирается до него.
Прокси-классы
Но как в эту схему вписываются модули, подключенные в классы из цепочки наследования? Их методы также должны быть видимы объекту класса, который стоит ниже в цепи (если считать, что BasicObject
стоит на самом верху).
Дело в том, что при включении класса руби создает анонимный класс, и помещает его в цепочку наследования прямо над включившим этот модуль классом. Такие анонимные классы часто называют прокси-классами. Соответственно, при поиске метода, определенного в модуле, он будет найден в анонимном прокси-классе, и все произойдет так, как если бы метод был определен в одном из настоящих классов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Синглтон-классы
В ruby есть специальная форма определения метода: def object.method_name
. Созданный таким образом метод называется синглтон-методом - он определен только для этого конкретного объекта.
1 2 3 4 5 6 7 8 |
|
Именно благодаря этой особенности в ruby работают классовые методы: каждый класс является объектом класса Class
, который предоставляет лишь необходимый минимум методов - new
, superclass
итд. Если же нам нужно определить классовый метод (который будет инстанс-методом с точки зрения класса Class
), обычно мы задаем его следующим образом:
1 2 3 4 5 |
|
Разумеется, в данном случае метод class_method
становится доступен только MyClass
, и никакому другому классу (на самом деле он также будет доступен дочерним классам MyClass
, но об этом чуть позже).
Часто для определения сразу нескольких классовых методов используется конструкция class << self
внутри определения класса; в этом случае методы, определенные в обычной форме (def method_name
), становятся классовыми методами. То же самое применимо и к обычным объектам - в общем случае конструкция выглядит как class << object
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Это все может выглядеть как магия, но на самом деле ruby руководствуется единой логикой. Посмотрим внимательнее, что тут происходит.
Как уже говорилось выше, объект - это просто группа инстанс-переменных и ссылка на класс, который в свою очередь содержит некоторое кол-во инстанс-методов. Но синглтон-методы видны только одному конкретному объекту, поэтому не могут находиться в классе объекта; в то же время метод не может находиться в объекте, так как только классы и модули могут содержать методы (предоставляя их своим объектам и объектам дочерних классов).
Решением этой загадки является синглтон-класс (также встречается термин eigenclass). Это эдакая прослойка между объектом и его номинальным классом (тем самым, который можно получить через object.class
). Все синглтон-методы объекта попадают в его синглтон-класс, и уже он является “реальным” классом объекта. В то же время, чтобы не терять возможность вызывать инстанс-методы, определенные в номинальном классе, синглтон-класс наследуется от него, поэтому поиск метода при его вызове начинается с синглтон-класса, дальше поднимается в номинальный класс, в его родителя и дальше вверх до BasicObject
.
1 2 3 4 5 6 7 8 9 |
|
Наследование синглтон-классов классов устроено несколько сложнее. Классы наследуют у своих родителей не только инстанс-методы, но и классовые методы - но, как мы знаем, они содержатся в синглтон-классе класса, который при вышеописанной схеме наследования не попадает в цепочку. Поэтому в ruby синглтон-класс класса наследуется от синглтон-класса его родителя. Звучит немного запутанно, поэтому лучше посмотреть на примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
К сожалению (а, может, и к счастью), синглтон-методы скрыты от глаз программиста в методах, инспектирующих цепочку наследования - ни Kernel#class
, ни Module#ancestors
их не показывают. Но, как видно из последнего примера, с помощью Object#singleton_class
и Class#superclass
это можно обойти.
self и текущий класс
В каждой точке кода определены так называемые “текущий объект” и “текущий класс”.
Про текущий объект мы уже поговорили - это объект, который доступен через псевдопеременную self
; к нему адресуются все вызовы методов без указания получателя и в нем ищутся все запрошенные инстанс-переменные.
Текущий класс - это тот класс, инстанс-методом которого становится определенный в этом месте метод.
1 2 3 4 5 6 |
|
К сожалению, в отличие от текущего объекта, который можно узнать через self
, текущий класс можно отслеживать только по коду.
Посмотрим все типичные случаи, имеющие собственные правила определения self
и текущего класса.
Верхний уровень
До того, как мы входим в определение какого-либо класса, self
указывает на main
, а текущим классом является Object
. Это объясняет тот факт, что методы, определяемые на верхнем уровне, вызываются из любого участка кода, причем без получателя (такой метод будет приватным, т.е. его нельзя вызывать с явным получателем; а self
указывает на объект класса Object
либо его потомка, поэтому метод будет доступен).
1 2 3 4 5 6 7 |
|
Определение класса
В начале объявления класса self
и текущий класс меняются на объявляемый класс. Пожалуй, это самый очевидный пример использования текущего класса - определяемые методы становятся инстанс-методами класса, в определении которого это происходит. Классовые макросы, вроде attr_accessor
, вызываются без указания получателя и попадают в self
, т.е. применяются на объявляемый класс.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Определение метода
В объявлении метода self
указывает на объект, являющийся получателем при вызове этого метода. Это, в свою очередь, самый очевидный пример смены текущего объекта.
1 2 3 4 5 6 7 8 9 |
|
Текущий класс в определении метода не меняется; внутри определения метода можно определить второй метод, и при вызове первого второй станет методом того же класса:
1 2 3 4 5 6 7 8 9 10 11 |
|
class << object
Как уже говорилось чуть выше, у каждого объекта есть свой синглтон-класс - собственный класс объекта, содержащий все его синглтон-методы и наследующий от его номинального класса. Для определения синглтон-метода достаточно формы def self.method_name
, но если необходимо поработать с синглтон-классом более плотно, на помощь приходит конструкция class << object
. Как только мы попадаем внутрь нее, self
и текущий класс меняются на синглтон-класс нашего объекта.
Когда речь идет об обычном объекте, мы можем определить для него сразу несколько синглтон-методов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
В более распространенной ситуации, когда class << self
используется в определении класса, в роли объекта выступает сам класс, и мы попадаем в синглтон-класс класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
BasicObject#instance_eval / Module#class_eval
Часто возникает необходимость открыть существующий объект и изменить его “внутренности” - изменить инстанс-переменные, не имеющие attr_writer
и недоступные снаружи; добавить пару синглтон-методов разом; а если объект является классом - то и инстанс-методы (синтаксис class MyClass ... end
не подходит, если название класса определяется в рантайме, и на класс просто указывает переменная). Для таких случаев есть BasicObject#instance_eval
и Module#class_eval
. Они также меняют self
и текущий класс.
BasicObject#instance_eval
instance_eval
- это интерпретация кода в контексте определенного объекта. Внутри блока self
соответствует самому объекту, а текущий класс - синглтон-классу объекта:
1 2 3 4 5 6 7 8 9 10 |
|
Впрочем, синглтон-методы обычно определяют в конструкции class << object
.
Интересное достоинство instance_eval
- в лаконичных DSL, где self
, указывающий на нужный разработчику объект, принимает на себя все вызовы методов без указания получателя. Например, так устроен механизм задания маршрутов в роутере rails - вызывается метод ApplicationName::Application.routes.draw, который передает блок методу eval_block, а тот в свою очередь запускает instance_exec
на объекте mapper
- и все методы resources
, get
, post
, member
, collection
вызываются с этим объектом в качестве получателя:
1 2 3 4 5 6 7 8 9 10 |
|
Module#class_eval
Метод class_eval
, как и его синоним module_eval
, определен в классе Module. В блоке, который передается в class_eval
, self
и текущий класс указывают на класс/модуль, на котором вызван этот метод. Фактически, это аналог повторного открытия класса, которое дает возможность обыкновенного monkeypatching: мы можем переопределять инстанс-методы класса и работать с инстанс-переменными самого класса.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Цепочки наследования
Мы уже знаем, что при вызове метода он ищется по всей цепочке наследования классов, начиная с синглтон-класса объекта-получателя и до BasicObject
. Рассмотрим все случаи наследования классов:
При объявлении нового класса:
- если указан класс-родитель, то новый класс наследуется от него
- если родитель не указан, новый класс наследуется от
Object
При включении модуля в класс создается прокси-класс, содержащий инстанс-методы модуля, и помещается в цепочку прямо над включающим классом
Синглтон-класс обычного объекта (не класса и не модуля) наследуется от номинального класса этого объекта (класс, который возвращает
object.class
)Синглтон-класс класса наследуется от синглтон-класса родителя исходного класса (кроме синглтон-класса
BasicObject
, который наследуется отClass
)
Таким образом, методы любого объекта состоят из его синглтон-методов, инстанс-методов модулей, подключенных в его синглтон-класс, инстанс-методов его номинального класса и подключенных в него модулей, потом родителя этого класса и его подключенных модулей, и так далее до BasicObject
.
При подключении в один класс нескольких модулей порядок в цепочке наследования определяется порядком подключения: подключенный последним модуль будет стоять сразу после класса, его подключившего; далее предпоследний подключенный модуль итд; т.е., например, последний модуль может переопределить метод, заданный в предпоследнем.
Классовые методы любого класса, в свою очередь, состоят из синглтон-методов этого класса и инстанс-методов модулей, подключенных в его синглтон-класс, далее синглтон-методов класса-родителя и инстанс-методов его модулей, итд до синглтон-методов BasicObject
; потом инстанс-методов классов Class
, Module
и Object
, модуля Kernel
и наконец класса BasicObject
(так как цепочка наследования синглтон-класса BasicObject
выглядит следующим образом: #BasicObject < Class < Module < Object < Kernel < BasicObject
).
Выводы
Резюмируя все вышеописанное:
Любой объект содержит группу инстанс-переменных и ссылку на класс.
Любой класс или модуль содержит группу инстанс-методов, и предоставляет их всем объектам, в чьих цепочках наследования он находится; также любой класс содержит ссылку на родительский класс.
Любой объект может иметь собственный синглтон-класс, содержащий его синглтон-методы; он наследуется от номинального класса исходного объекта.
Классы и модули являются объектами, и обладают всеми свойствами объектов (имеют свой класс и полученные от него методы, также могут иметь синглтон-класс).
Любой класс имеет свою цепочку наследования - сам класс, его родительский класс, родительский класс родительского класса итд до корня классовой иерархии - класса
BasicObject
.При подключении модуля в класс он помещается в цепочку наследования прямо над этим классом.
Синглтон-класс объекта наследуется от номинального класса объекта; за исключением синглтон-классов классов, каждый из которых наследуется от синглтон-класса родителя исходного класса.
При вызове метода любого объекта этот метод ищется во всех классах цепочки наследования класса исходного объекта, начиная с синглтон-класса и заканчивая
BasicObject
.При обращении к инстанс-переменной она ищется в текущем объекте (
self
); при вызове метода без указания получателя он будет вызван на текущем объекте.При объявлении метода в обычной форме (
def method_name
) он становится инстанс-методом текущего класса;При объявлении метода с указанием получателя (
def object.method_name
) он становится синглтон-методом указанного объекта.Указатели на текущий объект (
self
) и текущий класс меняются в зависимости от контекста.