Trait upcasting в Rust: год спустя после стабилизации

С Rust 1.86 trait upcasting наконец стабилизирован, устранив многолетний класс костылей в экосистеме. Разбираем, как расширили vtable, какие баги нашли на nightly и почему паттерны вроде dyn MyAny: Any стали нормой.

Trait upcasting в Rust: год спустя после стабилизации

В Rust есть целый класс костылей, который лежит буквально в каждом крейте с API на dyn старше пары лет. Методы вида assuper, asdebug, intodyndisplay — каждый делает одну и ту же тупую вещь: возвращает тот же самый &self, только под нужным трейт-объектом. Встроенно очевидная операция, которую больше десяти лет не могли завезти в язык. С Rust 1.86 (3 апреля 2025) все эти костыли стали не нужны. Trait upcasting наконец стабилизирован: &dyn Sub спокойно кастится в &dyn Super, если trait Sub: Super. Причём стабилизировали не с первого раза. В конце 2023-го фичу мерджили, потом откатывали из-за двух багов в соундности, потом чинили, потом снова находили баги. Только к весне 2025 получилось выкатить окончательно. Прошёл год стабильной жизни с фичей, экосистема переварила, паттерны поменялись. Разберём: как расширили vtable под эту штуку (спойлер: ещё в 2021-м, за четыре года до самой стабилизации), сколько багов в соундности нашли на nightly и в чём была суть, почему dyn MyAny: Any теперь одна из самых частых идиом, и почему Vec в Vec всё равно не летит.

Стабилизация trait upcasting: как это работает

Trait upcasting — это возможность преобразовать трейт-объект из более конкретного типа в более общий. Если у вас есть трейт Sub, который наследует трейт Super, то значение типа &dyn Sub можно неявно или явно привести к &dyn Super. Это работает и с Box , и с другими умными указателями. Раньше для этого приходилось писать методы-обёртки, которые просто переиспользовали тот же указатель, но с другим типом. Теперь компилятор делает это сам, и это стало возможным благодаря расширению vtable — таблицы виртуальных методов, которая хранится в каждом трейт-объекте.

Расширение vtable было реализовано ещё в 2021 году, за четыре года до стабилизации. В vtable добавили дополнительные слоты, которые указывают на методы супер-трейтов. Когда вы кастуете &dyn Sub в &dyn Super, компилятор просто переставляет указатель на начало vtable, где лежат методы Super. Это звучит просто, но реализация заняла годы из-за тонкостей с безопасностью типов и соундностью — гарантией того, что программа не может нарушить инварианты языка.

Почему стабилизация заняла так долго? В конце 2023 года фичу мерджили в nightly, но затем откатили из-за двух багов в соундности. Баги были связаны с тем, что в некоторых случаях можно было получить доступ к памяти, которая не была инициализирована, или вызвать метод с неправильными аргументами. Проблемы обнаружили не сразу, а после тщательного тестирования. Потом фичу чинили, снова находили баги, и только к весне 2025 года всё наконец заработало. За это время на nightly нашли несколько десятков проблем, но все они были исправлены до релиза.

Предыстория и контекст

Проблема отсутствия trait upcasting была известна с первых дней Rust. Ещё в 2015 году, когда трейт-объекты стали стабильными, разработчики столкнулись с тем, что нельзя просто так привести dyn Sub к dyn Super. Это было особенно болезненно для библиотек, которые работали с типажами вроде Any или Display. Каждый автор библиотеки изобретал свой велосипед: кто-то писал методы assuper, кто-то использовал трейт-объекты с дополнительными параметрами, кто-то вообще избегал dyn. В итоге в экосистеме накопились тысячи одинаковых костылей, которые усложняли чтение кода и создавали лишний шум.

Почему это так долго не могли завезти? Основная сложность была в том, что vtable в Rust — это не просто массив указателей на функции, а сложная структура с метаданными. Добавление новых слотов требовало изменения ABI, что могло сломать совместимость с уже скомпилированными крейтами. Кроме того, нужно было продумать взаимодействие с auto-трейтами и supertrait'ами, которые могут быть не только прямыми, но и транзитивными. Всё это требовало тщательного проектирования и долгих обсуждений в RFC.

Контекст шире: trait upcasting — это часть более крупной работы по улучшению работы с трейт-объектами в Rust. В последние годы команда языка активно работает над такими вещами, как dyn (динамические трейт-объекты с явным указанием размера), анонимные трейт-объекты, а также улучшение вывода типов для dyn. Стабилизация upcasting — это важный шаг, который делает язык более эргономичным и снижает порог входа для новичков.

Что это значит для пользователей?

Для конечных пользователей библиотек trait upcasting означает, что они могут писать более чистый код без дополнительных обёрток. Например, если у вас есть функция, которая принимает &dyn Display, а у вас есть &dyn Debug (где Debug: Display не является автоматическим, но вы можете реализовать свой трейт), вы можете просто передать его напрямую, если ваш трейт наследует Display. Это упрощает композицию и делает код более читаемым.

Однако есть важное ограничение: upcasting работает только для трейт-объектов, а не для обобщённых типов. То есть вы не можете преобразовать Vec в Vec автоматически. Это связано с тем, что Vec — инвариантен по отношению к типу элемента, и компилятор не может гарантировать безопасность такого преобразования. Для этого нужно явно пройтись по вектору и преобразовать каждый элемент. Это ограничение часто удивляет новичков, но оно является следствием системы типов Rust и её фокуса на безопасности.

Технические подробности: как расширили vtable

Технически vtable в Rust — это структура, которая содержит указатели на методы трейта, а также на методы супер-трейтов. При создании трейт-объекта компилятор генерирует vtable, которая включает все методы, доступные через данный трейт. Для upcasting нужно, чтобы vtable содержала методы всех супер-трейтов, даже если они не используются напрямую. Это увеличивает размер vtable, но позволяет безопасно кастовать указатель.

В реализации, которая была предложена в RFC 3324, vtable расширили, добавив в конец слоты для методов супер-трейтов. При касте указатель на vtable просто сдвигается на нужное смещение. Это делается за O(1) и не требует дополнительных проверок, потому что компилятор статически знает раскладку. Однако это привело к необходимости изменения ABI, что было сделано с сохранением обратной совместимости для старых крейтов.

Сравнение с другими языками: в C++ есть множественное наследование и виртуальные таблицы, которые поддерживают upcasting, но там это работает через динамическое приведение типов (dynamiccast), которое требует RTTI и может быть медленным. В Rust же upcasting — это чисто статическая операция, которая не добавляет накладных расходов в рантайме. Это одно из преимуществ Rust перед C++ в плане производительности.

Кого затронет и как

Разработчики библиотек — главные бенефициары. Им больше не нужно писать методы assuper и подобные. Это уменьшает объём кода и упрощает поддержку. Например, популярные крейты, такие как anyhow, thiserror, serde, могут упростить свои внутренности, хотя публичные API они вряд ли изменят, чтобы не ломать обратную совместимость.

Для бизнеса, который использует Rust в производстве, это означает снижение затрат на разработку и меньше вероятность ошибок, связанных с неправильной реализацией костылей. Особенно это актуально для компаний, которые активно используют трейт-объекты для плагинов или полиморфизма.

В России и СНГ Rust набирает популярность, особенно в финтехе и системном программировании. Многие компании уже перешли на Rust 1.86 или новее, и они могут сразу использовать trait upcasting. Однако есть и те, кто застрял на старых версиях из-за зависимостей, и им придётся подождать обновления.

Что будет дальше

Сейчас trait upcasting стабилен, и экосистема постепенно адаптируется. Ожидается, что в ближайших релизах появятся новые идиомы, которые используют эту возможность. Например, уже сейчас распространён паттерн dyn MyAny: Any для создания гетерогенных коллекций. В будущем возможно появление более сложных инструментов, таких как автоматическая генерация методов для upcasting в макросах.

Команда Rust продолжает работать над улучшением трейт-объектов. В планах — стабилизация dyn (динамических трейт-объектов с явным размером), что позволит более гибко управлять памятью. Также идут работы над улучшением вывода типов для dyn и над возможностью использовать upcasting в обобщённом коде. В долгосрочной перспективе это может привести к тому, что многие паттерны, которые сейчас требуют unsafe или сложных обходных путей, станут безопасными и эргономичными.

Итог

Trait upcasting — это долгожданная фича, которая устраняет целый класс костылей и делает Rust более эргономичным. Она стабилизирована с 3 апреля 2025 года, и за год экосистема успела адаптироваться. Если вы ещё не используете её в своих проектах, самое время обновиться и упростить код. Следите за новыми релизами Rust, чтобы не пропустить дальнейшие улучшения работы с трейт-объектами.