В этой короткой статье я расскажу о паттерне в Rust, который позволяет "сохранять" для последующего использования тип, переданный через обобщенный метод. Этот паттерн встречается в исходниках Rust-библиотек и я тоже иногда его использую в своих проектах. Мне не удалось найти в сети публикаций о нем, поэтому я дал ему свое название: "Замыкание обобщенного типа", и в этой статье хочу рассказать, что он из себя представляет, зачем и как его можно использовать. В Rust развитая статическая система типов и ее статических возможностей достаточно для, наверное, 80% случаев использования. Но бывает, что необходима динамическая типизация, когда требуется хранить объекты разных типов в одном и том же месте. Тут на помощь приходят типажи-объекты: они стирают реальные типы объектов, сводят их к некоему общему интерфейсу, заданному типажом, и дальше можно оперировать этими объектами уже как однотипными типажами-объектами. Это хорошо работает еще в половине случаев из оставшихся. Но как быть, если нам все-таки нужно восстанавливать стертые типы объектов при их использовании? Например, если поведение наших объектов задается таким типажом, который не может использоваться в качестве типажа-объекта. Это — обычная ситуация для типажей с ассоциированными типами. Как быть в таком случае? Для всех 'static-типов (то есть типов, не содержащих не статических ссылок) в Rust реализуется типаж Any, который позволяет осуществлять преобразование типажа-объекта dyn Any к ссылке на исходный тип объекта: Запустить Также у Box для этих целей имеется метод downcast. Такое решение подходит для тех случаев, когда исходный тип известен в месте работы с ним. Но что делать, если это не так? Что делать, если вызывающий код просто не знает об исходном типе объекта в месте его использования? Тогда нам нужно как-то запомнить исходный тип, взять его там, где он определен, и сохранить наряду с типажом-объектом dyn Any, чтобы потом последний привести к исходному типу в нужном месте. К обобщенным типам в Rust можно относиться как к переменным типа, в которые можно передавать те или иные значения типа при вызове. Но в Rust нет способа запомнить такой тип для дальнейшего его использования в другом месте. Тем не менее, есть способ запомнить весь функционал, использующий данный тип, вместе с этим типом. В этом и заключается идея паттерна "Замыкание обобщенного типа": код, использующий тип, оформляется в виде замыкания, которое сохраняется как обычная функция, потому что оно не использует никаких объектов окружения, кроме обобщенных типов. Давайте рассмотрим пример реализации. Пусть мы хотим сделать рекурсивное дерево, представляющее иерархию графических объектов, в котором каждый узел может быть либо графическим примитивом с дочерними узлами, либо компонетом — отдельным деревом графических объектов: Упаковка Node в структуре Component необходима, так как сама структура Component используется в Node. Теперь предположим, что наше дерево — это только представление некоторой модели, с которой оно должно быть связано. Причем у каждого компонента будет своя модель: Мы могли бы написать: Но этот код не будет работать так, как нам нужно. Потому что компонент должен иметь свою собственную модель, а не модель родительского элемента, который содержит в себе компонент. То есть, нам нужно: Запустить Мы переместили указание конкретного типа модели в метод new, а в самом компоненте храним модель и поддерево уже со стертыми типами. Теперь добавим метод use_model, который будет использовать модель, но не будет параметризован ее типом: Запустить Обратите внимание, что в компоненте мы сохраняем указатель на функцию, которая создана в методе new с помощью синтаксиса определения замыкания. Но все, что она должна захватывать извне — это тип Model, поэтому ссылку на сам компонент мы вынуждены передавать в эту функцию через аргумент. Теперь метод use_model можно использовать в контексте, где реальный тип Model неизвестен. Например, при рекурсивном обходе дерева, состоящем из множества различных компонентов с разными моделями. Если есть возможность вынести интерфейс компонента в типаж, допускающий создание типажа-объекта, то лучше так и поступить, и вместо самого компонента оперировать его типажом-объектом: Запустить Оказывается, замыкания в Rust могут захватывать не только объекты окружения, но и типы. При этом их можно интерпретировать как обычные функции. Это свойство становится полезным, когда требуется единообразно работать с различными типами не теряя о них информации, если типажи-объекты при этом не применимы. Надеюсь, эта статья поможет вам в использовании Rust. Поделитесь своими соображениями в комментариях. Rust разработчик