Метапрограммирование в Ruby: это все о self

Классы и объекты в Ruby связаны между собой достаточно затейливо и не сразу можно понять что к чему. Из-за особого назначения классов очень легко потерять из вида тот факт, что классы — это тоже объекты Ruby. От «обычных» объектов их отличает два вещи: они служат образцом для создания новых объектов и они являются частью иерархии классов. Все верно, классы могут иметь экземпляры себя (объекты), суперклассы (родителей) и подклассы (детей).

Если классы — это объекты, то у них должен быть свой собственный класс. Классом всех классов (как объектов) в Ruby является класс Class:

# один из способов создать новый класс
Dog = Class.new

# общепринятый способ создания класса
class Dog
# какая-то реализация собачьего поведения
end
Dog.class
=> Class

Путь поиска метода

Отношения между классами и объектами становятся важными в контексте пути поиска метода. Когда вы вызываете метод для какого-то объекта, сначала Ruby ищет этот метод в классе объекта. Если там метод найти не удается, то следующим будет проверен суперкласс класса объекта и так далее по всей иерархии классов до самого первого класса, которым в Ruby 1.9 является BasicObject. Диаграмма ниже иллюстрирует как Ruby обходит иерархию классов в поисках нужного метода:

Путь поиска метода для классов работает также. Когда вы вызываете метод класса, то сначала будет просмотрен класс объекта, коим как мы выяснили является класс Class, затем поиск продолжится в суперклассе Module и так далее до BasicObject.

Dog.class
=> Class
Class.superclass
=> Module
Module.superclass
=> Object
Object.superclass
=> BasicObject

И тоже самое визуально:

Как мы видим, BasicObject и Object являются корнем иерархии классов, а это значит, что все объекты, без разницы обычные они или это классы (как объекты), наследуют методы экземпляров класса, определенные в этих классах.

Синглтон-методы

Основная функция классов — определять поведение своих экземпляров. Например, общее поведение собак расположено в классе Dog. Однако в Ruby мы можем добавлять уникальное поведение отдельным объектам. Т.е. мы можем добавить методы отдельному объекту класса Dog, которые не будут доступны другим экземплярам этого класса. Чаще всего такие методы называют синглтон-методами, потому что они принадлежат только одному единственному объекту.

Еще раз, синглтон-методы определяются непосредственно для объекта, а не внутри класса этого объекта.

snoopy = Dog.new
def snoopy.alter_ego
"Red Baron"
end
snoopy.alter_ego
# => "Red Baron"
fido.alter_ego
# => NoMethodError: undefined method `alter_ego' for #<Dog:0x0000000190cee0>

Метаклассы

Как было отмечено выше, когда мы вызываем метод для какого-то объекта, то Ruby ищет его в классе объекта. В случае синглтон-методов очевидно, что они находятся вне класса объекта, потому что они недоступны другим экземплярам этого класса. Так где же они? И каким образом Ruby умудряется находить синглтон-методы наравне с обычными методами экземпляра класса без нарушения нормального пути поиска метода?

Оказывается, Ruby реализует это в характерном ему изящном стиле. Сначала создается анонимный класс, чтобы разместить в нем синглтон-методы объекта. Затем этот анонимный класс занимает роль класса объекта, а класс объекта становиться суперклассом анонимного класса. Таким образом обычный шаблон поиска метода остается неизменным — и синглтон-методы (в анонимном классе) и методы экземпляра класса (в суперклассе анонимного класса) будут найдены, следуя обычному пути поиска метода (смотри диаграмму ниже).

У этого динамически создаваемого анонимного класса много названий: метакласс, класс-ореол (ghost), синглтон-класс и айгенкласс (eigenclass). Слово «айген» пришло из немецкого и переводится примерно как «свой собственный».

Примечание переводчика: дальше по тексту (как и в оригинальном заголовке) поста автор использует термин «eigenclass», тем не менее, для перевода я выбрал термин «метакласс», т.к. он менее всего коробит слух русского уха, на мой взгляд конечно. Смысл от этого не меняется.

На заметку: в добавок к определению синглтон-методов, используя имя объекта (т.е. def snoopy.alter_ego), также можно использовать специальный синтаксис для доступа к метаклассу объекта:

class << snoopy
def alter_ego
"Red Baron"
end
end

Конструкция «class <<» открывает метакласс любого объекта, который вы укажите, и дает вам возможность работать непосредственно внутри области видимости этого метакласса.

Методы класса

Являясь объектами, классы тоже могут иметь синглтон-методы. По правде говоря, то что мы привыкли считать методами класса на самом деле является ничем иным как синглтон-методами — методами, которые принадлежат одному единственному объекту, который, так уж получилось, является классом. Как и любые синглтон-методы, методы класса располагаются внутри метакласса.

Методы класса могут быть определены различными способами:

class Dog
def self.closest_relative
"wolf"
end
end
class Dog
class << self
def closest_relative
"wolf"
end
end
end
def Dog.closest_relative
"wolf"
end
class << Dog
def closest_relative
"wolf"
end
end

Все примеры выше идентичны: все они открывают метакласс объекта Dog и определяют в нем метод (класса).

Разоблачение метаклассов

Метаклассы не только анонимны, в обычных обстоятельствах они скрыты от нас. И это не смотря на то, что они спроектированы быть первой остановкой на пути поиска метода:

class << Dog
def closest_relative
"wolf"
end
end
Dog.class
# => Class

В коде выше, класс Dog продолжает говорить нам, что его классом является класс Class, хотя мы добавили ему метод класса, что должно было повлечь за собой создание метакласса и замену класса объекта Dog на его метакласс. Тем не менее, есть способ, который поможет нам проявить метакласс объекта:

class Object 
def metaclass
class << self
self
end
end
end

Этот код может по началу привести в замешательство, поэтому давайте разберемся в нем более детально.

Прежде всего, мы знаем, что класс Object является предком абсолютно всех классов в Ruby, поэтому это хорошее место для определения методов, которые мы хотим сделать доступными везде и всюду.

Чтобы понять код выше, мы должны отследить где и как меняется значение self. Непосредственно внутри метода metaclass() self представляет собой объект, для которого мы этот метод вызвали. Далее мы открываем метакласс этого объекта с помощью синтаксиса «class <<». Теперь, когда мы находимся внутри области видимости метакласса, значение self меняется и ссылается теперь на метакласс объекта. Возвращая self из метакласса, мы вызываем встроенный в Ruby идентификатор объекта to_s, что позволяет нам увидеть проблеск неуловимого метакласса.

Вот как мы можем использовать метод metaclass() для выуживания метакласса.

class Dog
end
snoopy = Dog.new
=> #<Dog:0x00000001c4a170>
snoopy.metaclass
=> #<Class:#<Dog:0x00000001c4a170>>
snoopy.metaclass.superclass
=> Dog

Метод «to_s» в Ruby

Метод to_s определен в классе Object и возвращает строковое представление объекта, для которого он был вызван. Строка представляет собой композицию из имени класса объекта и уникального идентификатора объекта. Например, #<Mouse:0x00000001c4a170>.

Классы (экземпляры класса Class), такие как Class, Object или String, переопределяют этот метод, чтобы он просто возвращал их имя.

Dog.to_s
=> "Dog"

В действительности, метод to_s переопределяется в классе Module, который является суперклассом класса Class. Вот как описывается метод to_s в документации Ruby к классу Module:

Возвращает строку, представляющую модуль или класс. Для базовых классов и модулей — это их имя. Для синглтонов мы также показываем информацию об объекте, к которому они прикреплены.

Обратите внимание на замечание о синглтонах. Это объясняет почему когда мы вызываем to_s для метакласса snoopy мы получаем безымянный «Class», за которым следует идентификатор объекта, к которому этот класс прикреплен: #<Class:#<Dog:0x00000001c4a170>>. Вызвав to_s для суперкласса метакласса, который ссылается на оригинальный (не метакласс) класс Dog, мы получаем просто его имя: «Dog».

Метаклассы и наследование классов

Также как у объектов есть доступ к методам экземпляра класса, определенным в суперклассах их класса, также и у классов есть доступ к методам класса, определенным в их предках.

class Mammal
def self.warm_blooded?
true
end
end

class Dog < Mammal
def self.closest_relative
"wolf"
end
end

Dog.closest_relative
# => "wolf"
Dog.warm_blooded?
# => true

Еще одна задачка. Как у классов получается наследовать методы классов своих предков? Ведь суперкласс класса находится на пути поиска метода экземпляров класса, но его нет на пути поиска метода для самого класса.

Путь поиска метода для экземпляра класса Dog:

fido -> Dog -> Mammal -> Object...

Путь поиска метода для объекта Dog:

Dog -> Class -> Module -> Object...

Здесь опять используется интересный маневр, чтобы обеспечить неприкосновенность шаблона поиска метода, и в то же время предоставить доступ к методам класса суперкласса.

Прежде всего, когда мы определяем метод класса (синглтон-метод) для класса Dog, то создается метакласс. Затем этот метакласс становится классом объекта Dog вместо класса Class. Класс Class «выталкивается» выше по цепочке поиска методов и становится суперклассом метакласса. Теперь, когда мы определим метод класса для суперкласса класса Dog (например для Mammal), то созданный метакласс станет суперклассом метакласса объекта Dog, «выталкивая» класс Class еще выше. Поскольку это объяснение такое же прозрачное как нефть, то вам диаграмма, которая прольет свет на описанное выше.

Вот, получите. Путь поиска метода для объекта в Ruby во всей свой красе. Ну почти. Мы не рассмотрели куда здесь вписываются примеси модулей. Надеюсь, что у меня будет время показать вам это в другом посте.

Пост полностью скопирован с хабра, чтоб не потерять.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.