Выбрать главу

[]

Cat

[Cat, Dog]

Cat

Мы видим, что первый массив пуст, поскольку ни один тип не был зарегистрирован, хотя строка “Cat" может быть успешно разрешена, даже если после нее зарегистрирован связанный тип. Причина этого в том, что регистрация происходит во время компиляции, а разрешение — во время выполнения. Другими словами, регистрация модели происходит до того, как программа начнет выполняться, независимо от того, в каком месте исходного кода зарегистрированы типы.

После регистрации двух типов мы видим, что массив MODELS содержит их. Наконец, это еще раз показывает, что его можно было разрешить при вызове до или после регистрации связанного типа. Как упоминалось ранее в этой главе, макросы не имеют такой же типизации, как обычный код Crystal. Из-за этого к макросам невозможно добавлять ограничения типов. Это означает, что пользователь может передать в макрос .register_model все, что пожелает, что может привести к не столь очевидным ошибкам. Например, если они случайно передали "Time" вместо Time, это приведет к следующей ошибке: неопределенный метод макроса 'StringLiteral#resolve'. В следующем разделе мы собираемся изучить способ сделать источник ошибки более очевидным.

Создание пользовательских ошибок времени компиляции

Ошибки времени компиляции — одно из преимуществ компилируемого языка. Вы сразу же узнаете о проблемах, вместо того, чтобы ждать, пока этот код будет выполнен, чтобы обнаружить ошибку. Однако, поскольку Crystal не знает контекста конкретной ошибки, он всегда будет выводить одно и то же сообщение об ошибке одного и того же типа. Последняя функция, которую мы собираемся обсудить в этой главе, связана с выдачей ваших собственных ошибок во время компиляции.

Пользовательские ошибки времени компиляции могут быть отличным способом добавить дополнительную информацию к сообщению об ошибке, что значительно облегчает жизнь конечному пользователю, поскольку ему становится понятнее, что необходимо сделать для устранения проблемы. Возвращаясь к примеру в конце последнего раздела, давайте обновим наш макрос .exclude_type, чтобы обеспечить лучшее сообщение об ошибке в случае передачи неожиданного типа.

В последних нескольких главах мы использовали различные макрометоды верхнего уровня, такие как #env, #flag и #debug. Другой метод верхнего уровня — #raise, который вызывает ошибку во время компиляции и позволяет предоставить собственное сообщение. Мы можем использовать это с некоторой условной логикой, чтобы определить, не является ли значение, переданное нашему макросу, Path. Наш обновленный макрос будет выглядеть так:

macro exclude_type(type)

  {% raise %(Expected argument to 'exclude_type' to be

    'Path', got '#{type.class_name.id}'.) unless type.is_a?

      Path %}

  {% EXCLUDED_TYPES << type.resolve %}

end

Теперь, если бы мы вызвали макрос с "Time", мы бы получили ошибку:

In mutable_constants.cr:43:1

43 | exclude_type "Time"

     ^-----------

Error: Expected argument to 'exclude_type' to be 'Path', got 'StringLiteral'.

Помимо отображения нашего специального сообщения, он также выделяет вызов макроса, вызвавший ошибку, и показывает номер строки. Однако есть кое-что, что мы можем сделать, чтобы еще больше улучшить эту ошибку.

Все типы макросов, с которыми мы работали, произошли от базового типа макроса ASTNode, который предоставляет базовые методы, общие для всех узлов, откуда и берет свое начало метод #id, который мы использовали несколько раз. Этот тип также определяет свой собственный метод #raise, который работает так же, как и метод верхнего уровня, но выделяет конкретный узел, на котором он был вызван.

Мы можем реорганизовать нашу логику, чтобы использовать это, используя type.raise вместо простого повышения. К сожалению, в этом случае результирующая подсветка ошибок такая же. В Crystal есть несколько серьезных ошибок, связанных с этим, так что, надеюсь, со временем ситуация улучшится. Тем не менее, следовать этой практике по-прежнему рекомендуется, поскольку она не только дает читателю более ясное представление о том, что такое недопустимое значение, но также делает код пригодным для будущего.

Ограничение универсальных типов

Обобщенные шаблоны в Crystal обеспечивают хороший способ уменьшения дублирования, позволяя параметризовать тип для поддержки его использования с несколькими конкретными типами. Хорошим примером этого могут быть типы Array(T) или Hash(K, V). Однако обобщенные типы Crystal в настоящее время не предоставляют встроенного способа ограничения типов, с помощью которых может быть создан универсальный тип. Возьмем, к примеру, следующий код: