В этой главе мы рассмотрели, как определять и использовать аннотации для расширения различных Функции Crystal с дополнительными метаданными, включая способ хранения как именованных, так и позиционные аргументы, как читать одиночные и множественные аннотации и какие преимущества/Аннотации вариантов
Аннотации — это жизненно важная функция метапрограммирования, которую мы обязательно будем использовать в следующих главах. До сих пор весь макрокод, который мы писали для доступа к данным типа или метода, находился в контексте этого типа или метода.
В следующей главе мы собираемся изучить функцию самоанализа типов во время компиляции Crystal, которая представит новые способы доступа к той же информации.
12. Использование интроспекции типов во время компиляции
В предыдущих главах мы в основном использовали макросы внутри самих типов и методов для доступа к информации времени компиляции или чтения аннотаций. Однако это значительно снижает эффективность макросов, поскольку они могут динамически реагировать на добавление или аннотирование новых типов. Следующая концепция метапрограммирования Crystal, которую мы собираемся рассмотреть, — это интроспекция типов во время компиляции, которая будет охватывать следующие темы:
• Итерация переменных типа
• Итерационные типы
• Итерационные методы
К концу этой главы вы сможете создавать макросы, которые генерируют код, используя переменные экземпляра, методы и/или информацию о типе, а также данные, считываемые из аннотаций.
Технические требования
Требования к этой главе следующие:
• Рабочая установка Кристалла.
Инструкции по настройке Crystal можно найти в
Все примеры кода, использованные в этой главе, можно найти в папке
Итерация переменных типа
Одним из наиболее распространенных случаев использования интроспекции типов является перебор переменных экземпляра типа. Простейшим примером этого может быть добавление метода #to_h
к объекту, который возвращает хэш, используя переменные экземпляра типа для ключа/значений. Это будет выглядеть так:
class Foo
getter id : Int32 = 1
getter name : String = "Jim"
getter? active : Bool = true
def to_h
{
"id" => @id,
"name" => @name,
"active" => @active,
}
end
end
pp Foo.new.to_h
Который, когда будет выполнен, выведет следующее:
{"id" => 1, "name" => "Jim", "active" => true}
Однако это далеко не идеально, поскольку вам нужно не забывать обновлять этот метод
Мы могли бы улучшить его, используя макрос для перебора переменных экземпляра этого типа с целью построения хеша. Новый метод #to_h
будет выглядеть так:
def to_h
{% begin %}
{
{% for ivar in @type.instance_vars %}
{{ivar.stringify}} => @{{ivar}},
{% end %}
}
{% end %}
end
Если вы помните из #instance_vars
для экземпляра TypeNode
, полученного с помощью специальной макропеременной @type
. Этот метод возвращает Array(MetaVar)
, который включает информацию о каждой переменной экземпляра, такую как ее имя, тип и значение по умолчанию.
Наконец, мы перебираем каждую переменную экземпляра с помощью цикла for, используя строковое представление имени переменной экземпляра в качестве ключа и, конечно же, ее значение в качестве значения хеша. Запуск этой версии программы дает тот же результат, что и раньше, но с двумя основными преимуществами:
• Он автоматически обрабатывает вновь добавленные/удаленные переменные экземпляра.
• Он будет включать переменные экземпляра, определенные для дочерних типов, поскольку макрос расширяется для каждого конкретного подкласса, поскольку он использует макропеременную @type
.
Подобно итерации переменных экземпляра, доступ к переменным класса также можно получить с помощью метода TypeNode#class_vars
. Однако есть одна серьезная ошибка при переборе переменных экземпляра/класса типа.