Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
Ни один из этих вариантов в большинстве случаев не является отличным вариантом. Чтобы лучше решить эту проблему, нам нужно изучить следующую концепцию метапрограммирования Crystal: аннотации.
В этой главе мы рассмотрим следующие темы:
• Что такое аннотации?
• Хранение данных в аннотациях.
• Чтение аннотаций
К концу этой главы вы должны иметь четкое представление о том, что такое аннотации и как их использовать.
Технические требования
Требования к этой главе следующие:
• Рабочая установка Crystal.
Инструкции по настройке Crystal можно найти в Главе 1 «Введение в Crystal».
Все примеры кода, использованные в этой главе, можно найти в папке Chapter 11 на GitHub: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter11.
Что такое аннотации?
Проще говоря, аннотация — это способ прикрепить метаданные к определенным функциям кода, к которым впоследствии можно получить доступ во время компиляции внутри макроса. Crystal поставляется в комплекте с некоторыми встроенными аннотациями, с которыми вы, возможно, уже работали, например @[JSON::Field] или аннотацией @[Link], которая была рассмотрена в Главе 7, «Взаимодействие C». Хотя обе эти аннотации включены по умолчанию, они различаются по своему поведению. Например, аннотация JSON::Field существует в стандартной библиотеке Crystal и реализована/используется таким образом, что вы можете воспроизвести ее в своем собственном коде с помощью собственной аннотации. С другой стороны, аннотация Link имеет особые отношения с компилятором Crystal, и часть ее поведения не может быть воспроизведена в пользовательском коде.
Пользовательские аннотации можно определить с помощью ключевого слова annotation:
annotation MyAnnotation; end
Вот и все. Затем аннотацию можно было применить к различным элементам, включая следующие:
• Методы экземпляра и класса.
• Переменные экземпляра
• Классы, структуры, перечисления и модули.
Аннотацию можно применять к различным объектам, помещая имя аннотации в квадратные скобки синтаксиса @[], как в следующем примере:
@[MyAnnotation]
def foo
"foo"
end
@[MyAnnotation]
class Klass
end
@[MyAnnotation]
module MyModule
end
К одному и тому же элементу также можно применить несколько аннотаций:
annotation Ann1; end
annotation Ann2; end
@[Ann1]
@[Ann2]
@[Ann2]
def foo
end
В этом конкретном контексте на самом деле нет смысла использовать более одной аннотации, поскольку нет способа отличить их друг от друга; однако это будет иметь больше смысла, если вы добавите данные в аннотацию, что является темой следующего раздела.
Итак, аннотации — это то, что можно применять к различным вещам в коде для хранения метаданных о них. Но чем они на самом деле хороши? Основное преимущество, которое они предоставляют, заключается в том, что они не зависят от реализации. Другими словами, это означает, что вы можете просто аннотировать что-то, и соответствующая библиотека сможет читать из него данные без необходимости специального определения макроса для создания переменной экземпляра, метода или типа.
Примером этого может быть, скажем, у вас есть модель ORM, которую вы хотите проверить. Например, если одна из установленных вами библиотек использует собственный макрос, такой как column id : Int64, это может сделать другие библиотеки нефункциональными, поскольку аннотация может быть неправильно применена к переменной экземпляра или методу. Однако если все библиотеки используют аннотации, то все они работают со стандартными переменными экземпляра Crystal, поэтому у библиотек нет возможности конфликтовать, и это делает все более естественным.
Кроме того, аннотации более ориентированы на будущее и более гибки по сравнению с определениями макросов для этого конкретного варианта использования. Далее давайте поговорим о том, как хранить данные в аннотации.
Хранение данных в аннотациях
Подобно методу, аннотация поддерживает как позиционные, так и именованные аргументы:
annotation MyAnnotation
end
@[MyAnnotation(name: "value", id: 123)]
def foo; end
@[MyAnnotation("foo", 123, false)]
def bar; end
В этом примере мы определили два пустых метода, к каждому из которых применена аннотация. Первый использует исключительно именованные аргументы, а второй использует исключительно позиционные аргументы. Лучший пример применения нескольких аннотаций одного и того же типа можно продемонстрировать, когда в каждую аннотацию включены данные. Вот пример:
annotation MyAnnotation; end
@[MyAnnotation(1, enabled: false)]
@[MyAnnotation(2)]
def foo
end
Поскольку значения в каждой аннотации могут быть разными, связанная библиотека может создать несколько методов или переменных, например, на основе каждой аннотации и данных в ней. Однако эти данные бесполезны, если вы не можете получить к ним доступ! Давайте посмотрим, как это сделать дальше.
Чтение аннотаций
В Crystal вы обычно вызываете метод объекта, чтобы получить доступ к некоторым данным, хранящимся внутри. Аннотации ничем не отличаются. Тип Annotation предоставляет три метода, которые можно использовать для доступа к данным, определенным в аннотации, различными способами. Однако прежде чем вы сможете получить доступ к данным в аннотации, вам необходимо получить ссылку на экземпляр Annotation. Это можно сделать, передав тип Annotation методу #annotation, определенному для типов, поддерживающих аннотации, включая TypeNode, Def и MetaVar. Например, мы можем использовать этот метод для печати аннотации, примененной к определенному классу или методу, если таковой имеется:
annotation MyAnnotation; end
@[MyAnnotation]
class MyClass
def foo
{{pp @type.annotation MyAnnotation}}
{{pp @def.annotation MyAnnotation}}
end
end
MyClass.new.foo
Метод #annotation вернет NilLiteral, если аннотация указанного типа не применена. Теперь, когда у нас есть доступ к примененной аннотации, мы готовы начать чтение из нее данных!
Первый, наиболее простой способ — использование метода #[], который может показаться знакомым, поскольку он также используется, среди прочего, как часть типов Array и Hash. Этот метод имеет две формы: первая принимает NumberLiteral и возвращает позиционное значение по предоставленному индексу. Другая форма принимает StringLiteral, SymbolLiteral или MacroId и возвращает значение с предоставленным ключом. Оба этих метода вернут NilLiteral, если по указанному индексу или указанному ключу не существует значения.
Два других метода, #args и #named_args, не возвращают конкретное значение, а вместо этого возвращают коллекцию всех позиционных или именованных аргументов в аннотации в виде TupleLiteral и NamedTupleLiteral соответственно.
Прежде всего, давайте посмотрим, как мы можем работать с