{% for ivar, idx in T.instance_vars.select &.
annotation Metadata %}
{{ivar.name.stringify}} => (
PropertyMetadata({{@type}}, {{ivar.type.resolve}},
{{idx}}).new({{ivar.name.stringify}},
{{ivar.annotation(Metadata).named_args
.double_splat}})
),
{% end %}
} of String => MetadataBase
@name = {{(ann = T.annotation(ClassConfig)) ?
ann[:name] : T.name.stringify}}
{% end %}
end
getter property_metadata : Hash(String, MetadataBase)
getter name : String
end
Модуль Metadatatable теперь выглядит так:
module Metadatable
macro included
class_getter metadata : ClassMetadata(self)
{ ClassMetadata(self).new }
end
end
Большая часть логики такая же, как и в предыдущем примере, за исключением того, что вместо прямого возврата хеша метод .metadata теперь возвращает экземпляр ClassMetadata, который предоставляет хеш. В этом примере мы также представили еще одну аннотацию, чтобы продемонстрировать, как предоставлять данные, когда аннотацию можно применить к самому классу, например настройку имени с помощью @[ClassConfig(name: "MySpecialName")].
В следующем разделе мы рассмотрим, как можно использовать макросы и константы вместе для регистрации вещей, которые можно будет использовать/перебирать в более поздний момент времени.
Определение значения константы во время компиляции
Константы в Crystal постоянны, но не заморожены. Другими словами, это означает, что если вы определите константу как массив, вы не сможете изменить ее значение на String, но вы можете вставлять/извлекать значения в/из массива. Это, в сочетании с возможностью макроса получать доступ к значению константы, приводит к довольно распространенной практике использования макросов для изменения констант во время компиляции, чтобы впоследствии значения можно было использовать/перебирать в готовом перехватчике.
С появлением аннотаций этот шаблон уже не так полезен, как раньше. Тем не менее, это все равно может быть полезно, если вы хотите предоставить пользователю возможность влиять на некоторые аспекты вашей макрологики, и нет места для применения аннотации. Одним из основных преимуществ этого подхода является то, что его можно вызвать в любом месте исходного кода и при этом применить, в отличие от аннотаций, которые необходимо применять к связанному элементу.
Например, скажем, нам нужен способ регистрации типов во время компиляции, чтобы можно было разрешать их по имени строки во время выполнения. Чтобы реализовать эту функцию, мы определим константу как пустой массив и макрос, который будет помещать типы в константу массива во время компиляции. Затем мы обновим логику макроса, чтобы проверить этот массив и пропустить переменные экземпляра с типами, включенными в массив. Первая часть реализации будет выглядеть так:
MODELS = [] of ModelBase.class
macro register_model(type)
{% MODELS << type.resolve %}
end
abstract class ModelBase
end
class Cat < ModelBase
end
class Dog < ModelBase
end
Здесь мы определяем изменяемую константу, которая будет содержать зарегистрированные типы, сами типы и макрос, который будет их регистрировать. Мы также вызываем #resolve для типа, переданного макросу, поскольку типом аргумента макроса будет Path. Метод #resolve преобразует путь в TypeNode, который представляет собой типы переменных экземпляра. Метод #resolve необходимо использовать только в том случае, если тип передается по имени, например, в качестве аргумента макроса, тогда как макропеременная @type всегда будет TypeNode.
Теперь, когда у нас определена сторона регистрации, мы можем перейти к стороне времени выполнения. Эта часть представляет собой просто метод, который генерирует оператор case, используя значения, определенные в константах MODELS, например:
def model_by_name(name)
{% begin %}
case name
{% for model in MODELS %}
when {{model.name.stringify}} then {{model}}
{% end %}
else
raise "model unknown"
end
{% end %}
end
Отсюда мы можем пойти дальше и добавить следующий код:
pp {{ MODELS }}
pp model_by_name "Cat"
register_model Cat
register_model Dog
pp {{ MODELS }}
pp model_by_name "Cat"
После его запуска вы увидите следующее, напечатанное на вашем терминале: