Однако обратите внимание, что в отличие от предыдущих примеров здесь нет прямого родительского типа, от которого наследуются все типы, а также не существует конкретного модуля, включенного в каждый из них.
Итерация типов с определенной аннотацией
В Crystal Object
является самым верхним типом из всех типов. Поскольку все типы неявно наследуются от этого типа, мы можем использовать его в качестве базового родительского типа для фильтрации до нужных нам типов.
Однако, поскольку этот подход требует перебора
Например, этот подход необходим, если типы, которые вы хотите перебрать, еще не имеют какого-либо общего определяемого пользователем типа и/или включенного модуля. Однако, поскольку этот тип также является родительским типом для типов в стандартной библиотеке, вам потребуется какой-то способ его фильтровать, например, с помощью аннотации.
Код, фактически выполняющий фильтрацию, похож на предыдущие примеры, только с немного более сложной логикой фильтрации. В конечном итоге это будет выглядеть следующим образом:
{% for type in Object.all_subclasses.select {|t| (ann =
t.annotation(MyAnnotation)) && (ann[:id] == nil || ann[:id]
% 2 == 0) } %}
{{pp type}}
{% end %}
В этом случае мы используем ArrayLiteral#select
, потому что нам нужны только те типы, для которых этот блок возвращает true
. Логика отражает требования, которые мы упоминали ранее. Он выбирает типы, которые имеют нашу аннотацию и либо не имеют поля id
, либо поля id
с четным номером. При создании этого примера будут правильно напечатаны ожидаемые типы: Child
и Foo
.
Итерационные типы, включающие определенный модуль
Третий способ, которым мы можем перебирать типы, - это запросить те типы, которые включают определенный модуль. Это может быть достигнуто с помощью метода TypeNode#includers
, где TypeNode
представляет модуль, например:
module SomeInterface; end
class Bar
include SomeInterface
end
class Foo; end
class Baz
include SomeInterface
end
class Biz < Baz; end
{{pp SomeInterface.includers}}
Построение этой программы выведет следующее:
[Bar, Baz]
При использовании метода #includers
следует отметить, что он включает только типы, которые напрямую включают этот модуль, а не типы, которые затем наследуются от него. Однако затем можно было бы вызвать #all_subclasses
для каждого типа, возвращаемого через #includers
, если это соответствует вашему варианту использования. Конечно, здесь также применима любая из ранее упомянутых логик фильтрации, поскольку #includers
возвращает ArrayLiteral(TypeNode)
.
Во всех этих примерах мы начали с базового родительского типа и прошли через все подклассы этого типа. Также возможно сделать обратное; начните с дочернего типа и перебирайте его предков. Например, давайте посмотрим на предков класса Biz
, добавив в нашу программу следующий код и запустив его:
{{pp Biz.ancestors}}
Это должно вывести следующее:
[Baz, SomeInterface, Reference, Object]
Обратите внимание, что мы получаем прямой родительский тип, модуль, который включает в себя его суперкласс, и некоторые неявные суперклассы этого типа, включая вышеупомянутый тип Object
. И снова метод #ancestors
возвращает ArrayLiteral(TypeNode)
, поэтому его можно фильтровать, как мы это делали в предыдущих примерах.
Следующая особенность метапрограммирования, которую мы собираемся рассмотреть, — это перебор методов типа.
Итерационные методы
Итерирующие методы имеют много общего с итерирующими типами, только с другим типом макроса. Первое, что нам нужно для перебора методов, — это TypeNode
, представляющий тип, методы которого нас интересуют. Отсюда мы можем вызвать метод #methods
, который возвращает ArrayLiteral(Def)
всех методов, определенных для этого типа. Например, давайте напечатаем массив всех имен методов внутри класса: