Шрифт:
Интервал:
Закладка:
Наконец, мы перебираем каждую переменную экземпляра с помощью цикла for, используя строковое представление имени переменной экземпляра в качестве ключа и, конечно же, ее значение в качестве значения хеша. Запуск этой версии программы дает тот же результат, что и раньше, но с двумя основными преимуществами:
• Он автоматически обрабатывает вновь добавленные/удаленные переменные экземпляра.
• Он будет включать переменные экземпляра, определенные для дочерних типов, поскольку макрос расширяется для каждого конкретного подкласса, поскольку он использует макропеременную @type.
Подобно итерации переменных экземпляра, доступ к переменным класса также можно получить с помощью метода TypeNode#class_vars. Однако есть одна серьезная ошибка при переборе переменных экземпляра/класса типа.
ПРЕДУПРЕЖДЕНИЕ
Доступ к переменным экземпляра возможен только в контексте метода. Попытка сделать это вне метода всегда приведет к получению пустого массива, даже если используется в ловушке завершения макроса.
По сути, это ограничение компилятора Crystal на данный момент, которое может быть реализовано в той или иной форме в будущем. Но до тех пор лучше иметь это в виду, чтобы не тратить время на отладку чего-то, что просто не будет работать. Посетите https://github.com/crystal-lang/crystal/issues/7504 для получения дополнительной информации об этом ограничении.
Другой вариант использования итерации переменных экземпляра — это добавление переменных экземпляра к некоторой внешней логике, которая может быть включена в модуль. Например, предположим, что у нас есть модуль Incrementable, который определяет один метод #increment, который, как следует из названия, будет увеличивать определенные выбранные переменные. Реализация этого метода может использовать @type.instance_vars вместе с ArrayLiteral#select, чтобы определить, какие переменные следует увеличить.
Прежде всего, давайте посмотрим на код модуля Incrementable:
module Incrementable
annotation Increment; end
def increment
{% for ivar in @type.instance_vars.select &.annotation Increment %}
@{{ivar}} += 1
{% end %}
end
end
Сначала мы определяем наш модуль вместе с аннотацией внутри него. Затем мы определяем метод, который фильтрует переменные экземпляра типа только для тех, к которым применена аннотация. Для каждой из этих переменных мы увеличиваем ее на единицу. Далее давайте посмотрим на тип, который будет включать в себя этот модуль:
class MyClass
include Incrementable
getter zero : Int32 = 0
@[Incrementable::Increment]
getter one : Int32 = 1
getter two : Int32 = 2 @[Incrementable::Increment]
getter three : Int32 = 3
end
Это довольно простой класс, который просто включает в себя наш модуль, определяет некоторые переменные экземпляра с помощью макроса getter и применяет аннотацию, определенную в модуле, к паре переменных. Мы можем протестировать наш код, создав и запустив следующую небольшую программу:
obj = MyClass.new
pp obj
obj.increment
pp obj
В этой программе мы создаем новый экземпляр нашего класса, который мы определили в последнем примере, печатаем состояние этого объекта, вызываем метод increment, а затем снова печатаем состояние объекта. Первая строка вывода показывает, что значение каждой переменной экземпляра соответствует имени переменной. Однако вторая строка вывода показывает, что переменные номер один и три действительно были увеличены на единицу.
Конечно, этот пример довольно тривиален, но приложения могут быть гораздо более сложными и мощными, о чем мы подробнее поговорим в следующей главе. А пока давайте перейдем от итерации переменных экземпляра/класса к итерации типов.
Итерационные типы
Многое из того, о чем мы говорили и продемонстрировали в последнем разделе, также можно применить и к самим типам. Одним из основных преимуществ перебора типов является то, что они не ограничены теми же ограничениями, что и переменные экземпляра. Другими словами, вам не обязательно находиться в контексте метода, чтобы перебирать типы. Благодаря этому возможности практически безграничны!
Вы можете перебирать типы в контексте другого класса для генерации кода, перебирать на верхнем уровне для создания дополнительных типов или даже внутри метода, чтобы построить своего рода конвейер, используя аннотации для определения порядка.
В каждом из этих контекстов любые данные, доступные во время компиляции, могут использоваться для изменения способа генерации кода, например переменные среды, константы, аннотации или данные, извлеченные из самого типа. В общем, это очень мощная функция, имеющая множество полезных применений. Но прежде чем мы сможем начать исследовать некоторые из этих вариантов использования, нам сначала нужно узнать, как можно выполнять итерации типов. Существует четыре основных способа итерации типов:
1. По всем или прямым подклассам родительского типа.
2. Типы, включающие определенный модуль.
3. Типы, к которым применяются определенные аннотации*
4. Некоторая комбинация предыдущих трех способов.
Первые два довольно очевидны. Третий метод отмечен звездочкой, так как здесь есть одна проблема, которую мы обсудим чуть позже в этой главе. Четвертое заслуживает дальнейшего объяснения. По сути, это означает, что вы можете использовать комбинацию первых трех, чтобы отфильтровать нужные вам типы. Примером этого может быть перебор всех типов, которые наследуются от определенного базового класса и к которым применена определенная аннотация, имеющая поле с определенным значением.
Самый распространенный способ перебора типов — через подклассы родительского типа. Это могут быть либо все подклассы этого типа, либо только прямые подклассы. Давайте посмотрим, как бы вы это сделали.
Итерация подклассов типа
Прежде чем мы перейдем к более сложным примерам, давайте сосредоточимся на более простом варианте использования перебора подклассов типа с использованием следующего дерева наследования:
abstract class Vehicle; end
abstract class Car < Vehicle; end
class SUV < Vehicle; end
class Sedan < Car; end
class Van < Car; end
Первое, что нам нужно, это TypeNode родительского типа, подклассы которого мы хотим перебрать. В нашем случае это будет Vehicle, но это не обязательно должен быть самый верхний тип. Мы могли бы с тем же успехом выбрать Car, если бы она лучше соответствовала нашим потребностям.
Если вы помните первую главу этой части, мы смогли получить TypeNode с помощью специальной макропеременной @type. Однако это будет работать только в том случае, если