Все примеры кода, использованные в этой главе, можно найти в папке Chapter13 на GitHub: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter13.
Использование аннотаций для влияния на логику времени выполнения
Как мы узнали в Главе 11 «Введение в аннотации», аннотации — это отличный способ добавить дополнительные метаданные к различным функциям Crystal, таким как типы, переменные экземпляра и методы. Однако одним из их основных ограничений является то, что хранящиеся в них данные доступны только во время компиляции.
В некоторых случаях вам может потребоваться реализовать функцию с использованием аннотаций для настройки чего-либо, но логика, требующая этих данных, не может быть сгенерирована только с помощью макросов и должна выполняться во время выполнения. Например, предположим, что мы хотим иметь возможность печатать экземпляры объектов в различных форматах. Эта логика может использовать аннотации, чтобы отметить, какие переменные экземпляра следует предоставлять, а также настроить способ их форматирования. Высокоуровневый пример этого будет выглядеть так:
annotation Print; end
class MyClass
include Printable
@[Print]
property name : String = "Jim"
@[Print(format: "%F")]
property created_at : Time = Time.utc
@[Print(scale: 1)]
property weight : Float32 = 56.789
end
MyClass.new.print
Результатом этого может быть следующее:
---
name: Jim
created_at: 2021-11-16
weight: 56.8
---
Чтобы реализовать это, логика печати должна иметь доступ как к данным аннотации, так и к значению переменной экземпляра, которая должна быть напечатана. В нашем случае модуль Printable позаботится об этом, определяя метод, который обрабатывает итерацию и печатает каждую применимую переменную экземпляра. В конечном итоге это будет выглядеть так:
module Printable
def print(printer)
printer.start
{% for ivar in @type.instance_vars.select(&.annotation Print) %}
printer.ivar({{ivar.name.stringify}},
@{{ivar.name.id}},
{{ivar.annotation(Print).named_args.double_splat}})
{% end %}
printer.finish
end
def print(io : IO = STDOUT)
print IOPrinter.new(io)
end
end
Большая часть логики выполняется в методе #print(printer). Этот метод напечатает начальный шаблон, которым в данном случае являются три тире. Затем он использует макрос цикла for для перебора переменных экземпляра включающего типа. Переменные экземпляра фильтруются таким образом, что включаются только те, у которых есть аннотация Print. Затем для каждой из этих переменных вызывается метод #ivar на принтере с именем и значением переменной экземпляра, а также любых именованных аргументов, определенных в аннотации. Наконец, он печатает конечный образец, который также состоит из трех тире.
Для поддержки предоставления значений из аннотации мы также используем метод NamedTupleLiteral#double_splat вместе с Annotation#named_ args. Эта комбинация предоставит любые пары ключ/значение, определенные в аннотации, в качестве именованных аргументов для вызова метода.
Метод #print(io) служит основной точкой входа для печати экземпляра. Он позволяет предоставить пользовательский I/O, на который должны выводиться данные, но по умолчанию это STDOUT. I/O используется для создания другого типа, который фактически выполняет печать:
struct IOPrinter
def initialize(@io : IO); end
def start
@io.puts "---"
end
def finish
@io.puts "---"
@io.puts
end
def ivar(name : String, value : String)
@io << name << ": " << value
@io.puts
end
def ivar(name : String, value : Float32, *, scale :
Int32 = 3)
@io << name << ": "
value.format(@io, decimal_places: scale)
@io.puts
end
def ivar(name : String, value : Time, *, format : String
= "%Y-%m-%d %H:%M:%S %:z")
@io << name << ": "
value.to_s(@io, format)
@io.puts
end
end
Этот тип определяет начальный и конечный методы, а также перегрузку для каждого из поддерживаемые типы переменных экземпляра, каждый из которых имеет определенные значения и значения по умолчанию, связанные с этим тип. Используя отдельный тип с перегрузками, мы можем раньше отловить по ним ошибки. являются ошибками времени компиляции, например, если вы использовали аннотацию для неподдерживаемого введите или не указал значение в аннотации для обязательного аргумента. Этот пример показывает, насколько гибкими и мощными могут быть аннотации Crystal в сочетании с другими понятиями, такими как композиция и перегрузки. Однако бывают случаи, когда вы можете захотеть отделить логику от самого типа, например, чтобы сохранить вещи слабо связанный.