Vehicle
. Если вы хотите выполнить итерацию за пределами этого типа, вам нужно будет использовать полное имя родительского типа.
Когда у нас есть TypeNode
, мы можем использовать два метода в зависимости от того, что именно мы хотим сделать. TypeNode#subclasses
можно использовать для получения прямых подклассов этого типа. TypeNode#all_subclasses
можно использовать для получения всех подклассов этого типа, включая подклассы подклассов и так далее. Например, добавьте в файл следующие две строки вместе с показанным ранее деревом наследования:
{{pp Vehicle.subclasses}}
{{pp Vehicle.all_subclasses}}
В результате компиляции программы на консоль будут выведены две строки: первая — [Car, SUV]
, а вторая — [Car, Sedan, Van, SUV]
. Вторая строка длиннее, поскольку она также включает подклассы типа Car
, который не включен в первую строку, поскольку Van
и Sedan
не являются прямыми дочерними элементами типа Vehicle
.
Также обратите внимание, что массив содержит как конкретные, так и абстрактные типы. На это стоит обратить внимание, поскольку если бы вы захотели перебрать типы и создать их экземпляры, это не удалось бы, поскольку был бы включен абстрактный тип Car
. Чтобы этот пример работал, нам нужно отфильтровать список типов до тех, которые не являются абстрактными. Оба метода в предыдущем примере возвращают ArrayLiteral(TypeNode)
. По этой причине мы можем использовать метод ArrayLiteral#reject
для удаления абстрактных типов. Код для этого будет выглядеть так:
{% for type in Vehicle.all_subclasses.reject &.abstract? %}
pp {{type}}.new
{% end %}
Запуск этого в конечном итоге приведет к печати нового экземпляра типов Sedan
, Van
, и SUV
. Мы можем пойти дальше в этой идее фильтрации и включить более сложную логику, например, использование данных аннотаций для определения того, следует ли включать тип.
Например, предположим, что мы хотим получить подмножество типов, имеющих аннотацию, исключая те, у которых есть определенное поле аннотации. В этом примере мы будем использовать следующие типы:
annotation MyAnnotation; end
abstract class Parent; end
@[MyAnnotation(id: 456)]
class Child < Parent; end
@[MyAnnotation]
class Foo; end
@[MyAnnotation(id: 123)]
class Bar; end
class Baz; end
У нас пять занятий, включая одно реферативное. Мы также определили аннотацию и применили ее к некоторым типам. Кроме того, некоторые из этих аннотаций также включают поле id
, в котором установлено некоторое число. Используя эти классы, давайте переберем только те, у которых есть аннотация и либо нет поля id
, либо ID
является четным числом.
Однако обратите внимание, что в отличие от предыдущих примеров здесь нет прямого родительского типа, от которого наследуются все типы, а также не существует конкретного модуля, включенного в каждый из них. Итак, как мы собираемся отфильтровать нужный нам тип? Здесь в игру вступает звездочка в начале главы. Пока не существует прямого способа просто получить все типы с определенной аннотацией. Однако мы можем использовать один и тот же шаблон перебора всех подклассов типа, чтобы воспроизвести это поведение.
Итерация типов с определенной аннотацией
В Crystal Object
является самым верхним типом из всех типов. Поскольку все типы неявно наследуются от этого типа, мы можем использовать его в качестве базового родительского типа для фильтрации до нужных нам типов.
Однако, поскольку этот подход требует перебора всех типов, он гораздо менее эффективен, чем более целенаправленный подход. В будущем, возможно, появится лучший способ сделать это, но на данный момент, в зависимости от конкретного варианта использования/API, который вы хотите поддерживать, это достойный обходной путь.
Например, этот подход необходим, если типы, которые вы хотите перебрать, еще не имеют какого-либо общего определяемого пользователем типа и/или включенного модуля. Однако, поскольку этот тип также является родительским типом для типов в стандартной библиотеке, вам потребуется какой-то способ его фильтровать, например, с помощью аннотации.
Код, фактически выполняющий фильтрацию, похож на предыдущие примеры, только с немного более сложной логикой фильтрации. В конечном итоге это будет выглядеть следующим образом:
{% 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)
, поэтому его можно фильтровать, как мы это делали в предыдущих примерах.
Следующая особенность метапрограммирования, которую мы собираемся рассмотреть, — это