Выбрать главу

Все примеры кода, использованные в этой главе, можно найти в папке 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 в сочетании с другими понятиями, такими как композиция и перегрузки. Однако бывают случаи, когда вы можете захотеть отделить логику от самого типа, например, чтобы сохранить вещи слабо связанный.