Шрифт:
Интервал:
Закладка:
annotation MyClass; end
Annotation MyAnnotation; end
@[MyClass(true, id: "foo_class")]
class Foo
{% begin %}
{% ann = @type.annotation MyClass %}
{% pp "#{@type} has positional arguments of:
#{ann.args}" %}
{% pp "and named arguments of #{ann.named_args}" %}
{% pp %(and is #{ann[0] ? "active".id :
"not active".id}) %}
{% status = if my_ann = @type.annotation MyAnnotation
"DOES"
else
"DOES NOT"
end %}
{% pp "#{@type} #{status.id} have MyAnnotation applied." %}
{% end %}
end
Запуск этой программы выведет следующее:
"Foo has positional arguments of: {true}"
"and named arguments of {id: "foo_class"}"
"and is active."
"Foo DOES NOT have MyAnnotation applied."
Мы также можем сделать то же самое с аннотацией, примененной к методу:
annotation MyMethod; end
@[MyMethod(4, 1, 2, id: "foo")]
def my_method
{% begin %}
{% ann = @def.annotation MyMethod %}
{% puts "n" %}
{% pp "Method #{@def.name} has an id of #{ann[:id]}" %}
{% pp "and has #{ann.args.size} positional arguments" %}
{% total = ann.args.reduce(0) { |acc, v| acc + v } %}
{% pp "that sum to #{total}" %}
{% end %}
end
my_method
Запуск этой программы выведет следующее:
"Method my_method has an id of "foo""
"and has 3 positional arguments"
"that sum to 7"
В обоих этих примерах мы использовали все три метода, а также некоторые сами типы коллекций. Мы также увидели, как обрабатывать необязательную аннотацию, следуя той же логике обработки nil, что и в коде Crystal, не являющемся макросом. Если бы к нашему классу была применена аннотация, мы могли бы получить доступ к любым дополнительным данным из него через переменную my_ann, так же, как мы это делали с переменной ann в предыдущих строках. Этот шаблон может быть невероятно полезен, позволяя влиять на логику макроса наличием или отсутствием аннотации. Это может привести к более читабельному коду, для которого в противном случае потребовалась бы одна аннотация со множеством различных полей.
Как и в предыдущем примере с несколькими аннотациями для одного элемента, метод #annotation возвращает последнюю аннотацию, примененную к данному элементу. Если вы хотите получить доступ ко всем примененным аннотациям, вместо этого вам следует использовать метод #annotations. Этот метод работает почти идентично другому методу, но возвращает ArrayLiteral(Annotation) вместо Annotation?. Например, мы могли бы использовать этот метод для перебора нескольких аннотаций, чтобы напечатать индекс аннотации вместе со значением, которое она хранит:
annotation MyAnnotation; end
@[MyAnnotation("foo")]
@[MyAnnotation(123)]
@[MyAnnotation(123)]
def annotation_read
{% for ann, idx in @def.annotations(MyAnnotation) %}
{% pp "Annotation #{idx} = #{ann[0].id}" %}
{% end %}
end
annotation_read
Запуск этого приведет к печати следующего:
"Annotation 0 = foo"
"Annotation 1 = 123"
"Annotation 2 = 123"
Вот и все. Аннотации сами по себе являются довольно простой функцией, но могут быть весьма мощными в сочетании с некоторыми другими функциями метапрограммирования Crystal.
Резюме
В этой главе мы рассмотрели, как определять и использовать аннотации для расширения различных Функции Crystal с дополнительными метаданными, включая способ хранения как именованных, так и позиционные аргументы, как читать одиночные и множественные аннотации и какие преимущества/Аннотации вариантов использования выполняются поверх макросов.
Аннотации — это жизненно важная функция метапрограммирования, которую мы обязательно будем использовать в следующих главах. До сих пор весь макрокод, который мы писали для доступа к данным типа или метода, находился в контексте этого типа или метода.
В следующей главе мы собираемся изучить функцию самоанализа типов во время компиляции Crystal, которая представит новые способы доступа к той же информации.
12. Использование интроспекции типов во время компиляции
В предыдущих главах мы в основном использовали макросы внутри самих типов и методов для доступа к информации времени компиляции или чтения аннотаций. Однако это значительно снижает эффективность макросов, поскольку они могут динамически реагировать на добавление или аннотирование новых типов. Следующая концепция метапрограммирования Crystal, которую мы собираемся рассмотреть, — это интроспекция типов во время компиляции, которая будет охватывать следующие темы:
• Итерация переменных типа
• Итерационные типы
• Итерационные методы
К концу этой главы вы сможете создавать макросы, которые генерируют код, используя переменные экземпляра, методы и/или информацию о типе, а также данные, считываемые из аннотаций.
Технические требования
Требования к этой главе следующие:
• Рабочая установка Кристалла.
Инструкции по настройке Crystal можно найти в Главе 1 «Введение в Crystal».
Все примеры кода, использованные в этой главе, можно найти в папке Главы 12 на GitHub: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter12.
Итерация переменных типа
Одним из наиболее распространенных случаев использования интроспекции типов является перебор переменных экземпляра типа. Простейшим примером этого может быть добавление метода #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
Если вы помните из Главы 10 «Работа с макросами»,