Итерационные методы
Итерирующие методы имеют много общего с итерирующими типами, только с другим типом макроса. Первое, что нам нужно для перебора методов, — это TypeNode
, представляющий тип, методы которого нас интересуют. Отсюда мы можем вызвать метод #methods
, который возвращает ArrayLiteral(Def)
всех методов, определенных для этого типа. Например, давайте напечатаем массив всех имен методов внутри класса:
abstract class Foo
def foo; end
end
module Bar
def bar; end
end
class Baz < Foo
include Bar
def baz; end
def foo(value : Int32); end
def foo(value : String); end
def bar(x); end
end
baz = Baz.new
baz.bar 1
baz.bar false
{{pp Baz.methods.map &.name}}
Запуск этого приведет к следующему:
[baz, foo, foo, bar]
Обратите внимание, что, как и в случае с методом #includers
, выводятся только методы, явно определенные внутри типа. Также обратите внимание, что метод #foo
включается один раз для каждой из его перегрузок. Однако, несмотря на то, что #bar вызывается с двумя уникальными типами, он включается только один раз.
Логика фильтрации, о которой мы говорили в последнем разделе, также применима к итеративным методам. Проверка аннотаций может быть простым способом отметить методы, на которые должна воздействовать другая конструкция. Если вы вспомните модуль Incrementable
из первого раздела, вы легко можете сделать что-то подобное, но заменив переменные экземпляра методами. Методы также обладают дополнительной гибкостью, поскольку их не нужно повторять в контексте метода.
Если вы помните раздел об итерации переменных экземпляра ранее в этой главе, для доступа к переменным класса существовал специальный метод TypeNode#class_vars
. В случае методов класса эквивалентного метода не существует. Однако их можно перебирать. В большинстве случаев TypeNode
будет представлять тип экземпляра типа, поэтому он используется для перебора переменных экземпляра или методов экземпляра этого типа. Однако существует метод, который можно использовать для получения другого TypeNode
, представляющего метакласс этого типа, из которого мы можем получить доступ к методам его класса. Существует также метод, который возвращает тип экземпляра, если TypeNode
представляет тип класса.
Этими методами являются TypeNode#class
и TypeNode#instance
. Например, если у вас есть TypeNode
, представляющий тип MyClass
, первый метод вернет новый TypeNode
, представляющий MyClass.class
, тогда как последний метод превратит MyClass.class
в MyClass
. Когда у нас есть тип класса TypeNode
, это так же просто, как вызвать для него #methods
; например:
class Foo
def self.foo; end
def self.bar; end
end
{{pp Foo.class.methods.map &.name}}
Запуск этого приведет к следующему:
[allocate, foo, bar]
Вам может быть интересно, откуда взялся метод allocate
. Этот метод автоматически добавляется Crystal для использования в конструкторе, чтобы выделить память, необходимую для его создания. Учитывая, что вы, скорее всего, не захотите включать этот метод в свою логику, обязательно предусмотрите способ его отфильтровать.
Поскольку сами типы можно повторять, вы можете объединить эту концепцию с методами итерации. Другими словами, можно перебирать типы, а затем перебирать каждый из методов этого типа. Это может быть невероятно мощным средством автоматической генерации кода, так что конечному пользователю нужно только применить некоторые аннотации или наследовать/включить какой-либо другой тип.
Резюме
И вот оно у вас есть; как анализировать переменные, типы и методы экземпляра/класса во время компиляции! Этот метод метапрограммирования можно использовать для создания мощной логики генерации кода, которая может упростить расширение и использование приложений, одновременно делая приложение более надежным за счет снижения вероятности опечаток или ошибок пользователя.
Далее, в последней главе этой части, мы рассмотрим несколько примеров того, как все изученные до сих пор концепции метапрограммирования можно объединить в более сложные шаблоны/функции.
Дальнейшее чтение
Как упоминалось ранее, в TypeNode
есть гораздо больше методов, которые находятся за пределами области видимости. Однако я настоятельно рекомендую ознакомиться с документацией по адресу https://crystal-lang.org/api/Crystal/Macros/TypeNode.html, чтобы узнать больше о том, какие дополнительные данные могут быть извлечены.
13. Расширенное использование макросов
В последних нескольких главах мы рассмотрели различные концепции метапрограммирования, такие как макросы, аннотации, и то, как их можно использовать вместе, чтобы обеспечить самоанализ типов, методов и переменных экземпляра во время компиляции. Однако по большей части мы использовали их самостоятельно. Эти концепции также можно комбинировать, чтобы создавать еще более мощные шаборны! В этой главе мы собираемся изучить некоторые из них, в том числе:
• Использование аннотаций для влияния на логику времени выполнения.
• Представление данных аннотаций/типов во время выполнения.
• Определение значения константы во время компиляции.
• Создание собственных ошибок времени компиляции.
К концу этой главы вы должны иметь более глубокое понимание метапрограммирования в Crystal. У вас также должны быть некоторые идеи о неочевидных вариантах использования метапрограммирования, которые позволят вам создавать уникальные решения проблем в вашем приложении.
Технические требования
Прежде чем мы углубимся в эту главу, в вашей системе должно быть установлено следующее:
• Рабочая установка Crystal.
Инструкции по настройке Crystal можно найти в Главе 1 «Введение в Crystal».
Все примеры кода, использованные в этой главе, можно найти в папке 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