Приветствую. В Rust есть целый класс костылей, который лежит буквально в каждом крейте с API на dyn старше пары лет. Методы вида as_super, as_debug, into_dyn_display.

Крутанёшь поиск по большим репозиториям на GitHub, найдёшь такое тысячами. Каждый делает одну и ту же тупую вещь: возвращает тот же самый &self, только под нужным трейт-объектом. Встроенно очевидная операция, которую больше десяти лет не могли завезти в язык.

С Rust 1.86 (3 апреля 2025) все эти костыли стали не нужны. Trait upcasting наконец стабилизирован: &dyn Sub спокойно кастится в &dyn Super, если trait Sub: Super. Причём стабилизировали не с первого раза. В конце 2023-го фичу мерджили, потом откатывали из-за двух багов в соундности, потом чинили, потом снова находили баги. Только к весне 2025 получилось выкатить окончательно.

Прошёл год стабильной жизни с фичей, экосистема переварила, паттерны поменялись. Разберём: как расширили vtable под эту штуку (спойлер: ещё в 2021-м, за четыре года до самой стабилизации), сколько багов в соундности нашли на nightly и в чём была суть, почему dyn MyAny: Any теперь одна из самых частых идиом, и почему Vec<Box<dyn Sub>> в Vec<Box<dyn Super>> всё равно не летит.

База

Начнём с основы.

Приведение трейт-объекта — это когда &dyn Sub превращается в &dyn Super, где trait Sub: Super. Термин пришёл из ООП: там upcast значит «привести к базовому классу». В Rust классов нет, но super-trait bounds есть. Если Sub: Super, любой тип, реализующий Sub, обязан реализовать Super. Композиция похожая, идея та же.

С конкретными типами всё было нормально всегда. Есть у нас let x = MyType;, и MyType реализует и Sub, и Super, тогда и &x as &dyn Sub, и &x as &dyn Super работали с Rust 1.0. Проблема начиналась там, где конкретный тип уже потерян под dyn:

fn take_super(x: &dyn Super) { x.base(); }

fn call_it(sub: &dyn Sub) {
    take_super(sub);  // до 1.86: expected `&dyn Super`, found `&dyn Sub`
}

Компилятор знал, что каждый Sub также Super, но всё равно спотыкался. Почему?

Тут надо вспомнить, из чего состоит fat pointer. Два указателя: первый на данные, второй на vtable. Vtable отдельная для каждой пары (конкретный тип, трейт). То есть для одного и того же Button компилятор строит разные таблицы под Button as Sub и под Button as Super.

Механика приведения такая. На входе (data_ptr, vtable_Sub_ptr). На выходе нужен (data_ptr, vtable_Super_ptr). Указатель на данные тот же, всё ок. А указатель на vtable супертрейта откуда брать? В рантайме на руках только vtable трейта Sub, из которого никакой указатель на vtable Super не восстановить. Т

Один способ решить — держать снаружи таблицу (trait_id, type_id) → vtable_ptr и делать поиск в рантайме. Дорого и как-то стремновато, отвергли сразу. Другой способ — класть указатель на vtable супертрейта прямо внутрь vtable субтрейта, чтобы приведение было одним чтением из памяти. Этот вариант в итоге и взяли, но чтобы туда дойти, потребовалось много лет.

История долгая. Первые упоминания были ещё в середине 2010-х в паре RFC, но конкретная реализация не прописывалась. Позже фича приземлилась на nightly, где сидела годами. Со стабилизацией не задалось: первую попытку не приняли, вторую смёржили и через какое-то время откатили. Нашлись два бага с кастами сырых указателей. Пофиксили, сделали касты строже. Перед третьей попыткой всплыл ещё один баг, уже с нормализацией типов при построении раскладки vtable. Тоже закрыли. И только тогда приведение доехало до стабильного релиза.

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

Vtable расширили заранее

Само же расширение vtable под приведение произошло не при стабилизации самой фичи, а заметно раньше. Формат vtable в rustc никогда не был стабильным ABI, всегда считался деталью реализации. В какой-то момент команда компилятора переделала раскладку: теперь для каждого дополнительного супертрейта в vtable генерируется отдельный вспомогательный vtable (записи TraitVPtr). Подготовительный шаг под будущее приведение, но никакой возможности им пользоваться пока не было. На стабильном компиляторе &dyn Sub -> &dyn Super не компилировался, а структура vtable уже несла лишние байты.

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

Правда, сам расчёт раскладки vtable вместе с этим усложнился: для трейтов с несколькими супертрейтами добавились рекурсивные обходы иерархии. Позже на nightly всплыли баги, связанные именно с этой расчётной логикой (расскажу ниже). Так что платили за это не одними байтами в бинарнике, а ещё и слегка возросшим временем компиляции для трейт-иерархий.

Как расширили vtable: обход снизу вверх и плоский первый супертрейт

Классический vtable в Rust устроен оч просто. Шапка с метаданными и массив указателей на методы. Схематично на x86_64:

Offset  Field
0x00    drop_in_place
0x08    size_of
0x10    align_of
0x18    method_0
0x20    method_1
...     ...

Шапка одна и та же для любого трейта. Дальше указатели на методы в порядке объявления, с поправкой на методы по умолчанию и обобщённые методы (последние в vtable не попадают, вместо них вставляются пустые записи).

С 1.56 (напоминаю) к этому добавились слоты под vtable супертрейтов. Не по одной схеме, компилятор комбинирует две.

Первый супертрейт: плоская раскладка. Vtable трейта Sub физически начинается с раскладки vtable своего первого супертрейта. То есть если trait Sub: SuperA + SuperB, то vtable Sub начинается с drop_in_place, size, align, SuperA::method_0, SuperA::method_1, ..., Sub::method_0, Sub::method_1. Приведение &dyn Sub -> &dyn SuperA не требует ничего: указатель на vtable Sub одновременно годится и как указатель на vtable SuperA, потому что префикс совпадает.

Остальные супертрейты: указатели. Для второго, третьего и так далее супертрейтов в vtable Sub кладутся дополнительные указатели на их vtables:

Offset  Field
0x00    drop_in_place
0x08    size_of
0x10    align_of
0x18    SuperA::method_0     ← plain: SuperA лежит здесь целиком
0x20    SuperA::method_1
0x28    Sub::method_0
0x30    Sub::method_1
0x38    &VTABLE_Type_as_SuperB   ← указатель: остальные супертрейты через ссылку
0x40    &VTABLE_Type_as_SuperC

Приведение к SuperA бесплатное (просто подмена типа указателя). Приведение к SuperB это mov rsi, [rsi + 0x38], одно чтение из памяти. То же для SuperC.

Алгоритм построения такой раскладки в исходниках rustc называется обходом дерева супертрейтов снизу вверх (post-order). Компилятор:

  1. Строит дерево всех транзитивных супертрейтов (с дубликатами при ромбовидном наследовании).

  2. Обходит его снизу вверх.

  3. Для листа (супертрейт без своих супертрейтов) кладёт шапку (drop_in_place, size, align) плюс его методы.

  4. Для внутренних узлов встраивает свои методы после того, что дал первый супертрейт (плоская раскладка), плюс указатели на vtables остальных супертрейтов.

Была развилка в дизайне. Альтернатива — всегда использовать плоскую раскладку для всех супертрейтов, никаких слотов-указателей. Тогда приведение всегда бесплатное, ноль инструкций. Проблема, экспоненциальный рост размера. Ромбовидное наследование глубиной N даёт O(2^N) байт в vtable. Отвергли по причине встраиваемых систем, где такой рост неприемлем.

Есть ещё интересная штука. Порядок супертрейтов в объявлении влияет на скорость. Первый супертрейт бесплатно приводится через плоскую раскладку, остальные через чтение из памяти. Если есть trait Sub: SuperA + SuperB и в горячем пути чаще идёт приведение к SuperB, стоит поменять местами: trait Sub: SuperB + SuperA. Компилятор про это молчит, никакого lint нет. Из-за этого в некоторых обсуждениях RFC предлагали такой lint («put the most commonly used supertrait first»), но пока не приняли. На проде я такое видел один раз, разница в бенчмарке была вообще минимальная.

Одно чтение из памяти, никаких вызовов

Разберём, что вообще происходит при let s: &dyn Super = sub;.

Заходим в функцию с sub: &dyn Sub. На x86_64 fat pointer лежит в двух регистрах, скажем rdi (данные) и rsi (vtable). Рассмотрим два случая.

Случай 1: Super, первый супертрейт Sub. Компилятор использует плоскую раскладку, приведение ничего не требует:

ничего не делаем, регистры (rdi, rsi) уже содержат корректный fat pointer для &dyn Super.

Случай 2: Super, второй или дальше супертрейт. Компилятор знает, что в vtable Sub на известном заранее смещении лежит указатель на vtable Super. К примеру смещение 0x28:

mov rsi, [rsi + 0x28]  ; загрузили vtable супертрейта из vtable субтрейта
; всё, (rdi, rsi) теперь корректный fat pointer для &dyn Super

Одна инструкция. Чтение из L1 около 4–5 циклов. Vtable почти всегда горячий (его уже трогали, когда вызывали метод Sub), так что обычно попадание в L1. Итог: полциклы поверх нормальной работы.

Теперь для сравнения, эмуляция приведения через ручной метод as_super в самом трейте. Пишешь sub.as_super()— это виртуальный вызов через vtable. Одно чтение указателя метода as_super из vtable. Один непрямой вызов с потенциальным промахом предсказателя (около 10 циклов на промах). Внутри тела метода собирается fat pointer из self as &dyn Super, что требует ещё одного чтения статической vtable и сборки fat pointer.

Итог: минимум 4-5 инструкций против одной для случая «второй и дальше супертрейт», и против нуля для первого. Плюс риск промаха предсказателя.

Проверил через cargo-show-asm пару своих старых мест, где раньше был as_super. Экономия обычно 2-3 инструкции. Плюс один слот в vtable под as_super, который теперь не нужен.

С Box, Arc, Rc всё то же самое. Box<dyn Sub> в Box<dyn Super> — это преобразование на этапе компиляции, никакой аллокации и никакого движения данных. Внутри Box лежит fat pointer, приведение работает с ним так же, как для ссылки: заменяем указатель на vtable, всё.

Кстати, Arc<dyn Sub> в Arc<dyn Super> тоже не трогает счётчики. Это тот же указатель на тот же аллоцированный блок, просто с другим vtable в fat pointer. Атомарки не мигают, счётчик не растёт, просто замена одного слова в fat pointer.

Два раунда багов с корректностью

История с корректностью заслуживает отдельного разговора. Приведение чуть не стабилизировали раньше, но тогда пришлось откатывать.

Раунд первый. Стабилизацию смёржили, всё вроде хорошо. Через какое-то время в rustc нашли два похожих кейса: касты сырых указателей между разными объектами dyn могли давать некорректный vtable. Оба бага использовали то, что раньше у *const dyn Trait не было гарантии «vtable должен быть валидным для Trait». Соответственно, можно было сконструировать через касты указатель с чужим vtable, а потом попытаться сделать через него приведение и получить UB. Стабилизацию откатили. Потом всё закрыли: касты указателей сделали строже, и теперь сырой указатель к dyn Trait обязан нести валидный vtable.

Раунд второй. Следующая попытка стабилизации, тесты зелёные. И тут находят ещё один баг, на этот раз про нормализацию типов. Раскладка vtable для обобщённых супертрейтов с projections вычислялась через structural equality, без нормализации. Из-за этого одна и та же реализация могла давать разные раскладки в разных местах компиляции. Минимальный воспроизводящий пример:

#![feature(trait_upcasting)]

trait Supertrait<T> {
    fn _print_numbers(&self, mem: &[usize; 100]) {
        println!("{mem:?}");
    }
}
impl<T> Supertrait<T> for () {}

trait Identity { type Selff; }
impl<Selff> Identity for Selff { type Selff = Selff; }

trait Middle<T>: Supertrait<()> + Supertrait<T> {
    fn say_hello(&self, _: &usize) { println!("Hello!"); }
}
impl<T> Middle<T> for () {}

trait Trait: Middle<<() as Identity>::Selff> {}
impl Trait for () {}

fn main() {
    (&() as &dyn Trait as &dyn Middle<()>).say_hello(&0);
}

При построении vtable для dyn Trait компилятор видел супертрейт Middle<<() as Identity>::Selff>, у которого свои два супертрейта: Supertrait<()> и Supertrait<<() as Identity>::Selff>. Они идентичны (потому что <() as Identity>::Selff == ()), но структурно как токены разные. Компилятор их не дедуплицировал, раскладка получалась с двумя указателями на трейты. При приведении к dyn Middle<()> эти два супертрейта уже нормализованы в один, раскладка получалась с одним указателем. Смещения расходятся, и вместо say_hello вызывается printnumbers, который получает мусор со стека:

[0, 2338324182462507040, 7738151096083899748, 438881233243824, ...]

Фикс добавил нормализацию типов при построении раскладки vtable. Плюс похожий кейс через binder equality для higher-ranked супертрейтов, закрыли в том же патче.

dyn MyAny: главное практическое применение

Есть каноничный паттерн, который в блоге к релизу 1.86 указан как основное применение приведения. Он крутой:

use std::any::Any;

trait MyAny: Any {}

impl dyn MyAny {
    fn downcast_ref<T: 'static>(&self) -> Option<&T> {
        (self as &dyn Any).downcast_ref()
    }
}

Объявляем свой трейт MyAny, наследуемый от Any. Любой тип, реализующий MyAny, автоматически реализует Any (это про super-trait bound). Дальше через приведение к dyn Any получаем доступ к Any::downcast_ref, не добавляя никаких методов в MyAny.

До 1.86 такое не работало. Приходилось либо тянуть downcast_ref руками в MyAny (что не имеет смысла, потому что реализация всегда одна и та же), либо использовать crate вроде downcast-rs, который через магию макросов генерировал эти методы. Сейчас: две строчки, стандартная библиотека, ноль зависимостей.

Практическое использование этого паттерна: везде, где раньше сидела связка Any плюс TypeId::downcast_ref. Регистрация плагинов, шина событий, компонентные архитектуры, штуки похожие на ECS. Объявляешь свой доменный трейт, наследуешь от Any, получаешь и полиморфизм по трейту, и type-safe приведение к конкретному типу. В bevy, aws-sdk и некоторых частях tokio за прошедший год этот паттерн активно вошёл в код.

Как это поменяло дизайн API за год

Год стабильной жизни с фичей — это достаточный срок, чтобы посмотреть на код в проектах и увидеть, где реально стало лучше, а где по-старому.

Плагинные системы. До 1.86 если плагин реализует несколько трейтов (EventHandler, Renderer, Serializable), приходилось выбирать один «главный» трейт с методами-путями до остальных:

trait Plugin: EventHandler + Renderer + Serializable {
    fn as_event_handler(&self) -> &dyn EventHandler;
    fn as_renderer(&self) -> &dyn Renderer;
    fn as_serializable(&self) -> &dyn Serializable;
}

Каждая реализация Plugin, три метода-заглушки, три слота в vtable, съеденных не пойми зачем. Это можно было генерировать процедурным макросом, но всё равно тухло.

С 1.86:

trait Plugin: EventHandler + Renderer + Serializable {}

let plugin: &dyn Plugin = /* ... */;
let handler: &dyn EventHandler = plugin;
let renderer: &dyn Renderer = plugin;

Тут без комментариев.

Middleware. Tower, axum, hyper и вся эта братия. Middleware обычно реализует Service, плюс опционально Layer, HealthCheck, Metrics. До 1.86 либо всё пихалось в один трейт на 20 методов, либо жили с ручными as_* методами. Сейчас middleware может лежать как dyn Layer в главном стеке, и по требованию приводиться до dyn Service для реального вызова, до dyn Metrics для отдельной ветки метрик. Разделение обязанностей чище, диспатч дешевле.

Шина событий и компонентные архитектуры. Игры, симуляции, штуки в духе ECS. Компоненты реализуют несколько трейтов-слушателей. Раньше в bevy и подобных экосистемах регистрация шла через Any плюс TypeId::downcast_ref. Это поиск в рантайме, часто с HashMap внутри, плюс 'static bound на всё подряд. Сейчас можно делать статическую иерархию, dyn Component: UpdateListener + RenderListener + InputListener, и приводить к нужному слушателю на этапе компиляции. Один mov вместо поиска по HashMap.

Фреймворки GUI. Виджеты реализуют кучу трейтов: Widget, Draw, Layout, EventTarget, часто Debug. Раньше API либо жили в мега-трейте на 30 методов, либо в ручных as_* костылях. Сейчас можно чисто разложить обязанности по маленьким трейтам, объединить супертрейтом и жить.

Обработка ошибок. std::error::Error имеет Debug + Display как супертрейты. То есть любой тип, реализующий Error, автоматически имеет Debug и Display. До 1.86 передать Box<dyn MyError> куда-нибудь, где ожидают Box<dyn Error>, было проблемой: нужны были реализации From или ручные обёртки. С 1.86 автоматически:

trait MyError: std::error::Error + Send {}

fn top_level(e: Box<dyn MyError>) -> Box<dyn std::error::Error> {
    e  // просто работает
}

То же самое с dyn MyError в dyn Display или dyn Debug. Иерархии ошибок со своими трейтами стали существенно проще.

По ощущениям самый большой сдвиг произошёл там, где раньше использовалась связка Any + downcast_ref. Такой код за прошлый год массово переехал на иерархию трейтов с приведением. Причина двойная. Скорость (это на этапе компиляции вместо рантайма) и type safety: приведение не может провалиться, а downcast_ref может, и обрабатывать этот случай надо везде.

Multi-trait dyn: почему dyn A + B до сих пор не летит

Есть паттерн, который часто хочется, но не получается: Box<dyn Read + Write>. Не dyn ReadWrite через супертрейт, а прямо два трейта под одной обёрткой.

Rust вроде бы позволяет писать dyn A + B + C, но с ограничением: только один из A, B, C может быть «настоящим» трейтом с методами. Остальные обязаны быть авто-трейтами (Send, Sync, Unpin) или ограничением времени жизни ('static). То есть dyn Iterator + Send + Sync пойдёт, а dyn Iterator + Debug уже нет: оба реальные трейты.

Все это из-за того, что Fat pointer имеет один указатель на vtable, и только один. Чтобы dyn Iterator + Debug работал, нужно было бы либо два указателя на vtable в fat pointer (что удваивает его размер с 16 до 24 байт), либо объединённый vtable с методами обоих трейтов.

Второй вариант комбинаторно взрывается: для каждой уникальной пары трейтов нужна отдельная раскладка vtable, и всё это должно где-то лежать в бинарнике. Плюс поверх этого пришлось бы уметь приводить dyn A + B к любому подмножеству (к dyn A, к dyn B, к dyn A + C из тройки, и так далее), а это ещё больше раскладок. Не тривиальная задача.

С приведением это местами обходится через промежуточный супертрейт:

trait DebugIter: Iterator + Debug {}
impl<T: Iterator + Debug> DebugIter for T {}

let x: &dyn DebugIter = /* ... */;
let iter: &dyn Iterator = x;  // приведение
let dbg: &dyn Debug = x;      // приведение

Промежуточный трейт DebugIter объединяет нужные трейты, и из объекта dyn можно спокойно привестись к любому из супертрейтов. Небольшой бойлерплейт (объявить и реализовать промежуточный трейт), но работает без стоимости в рантайме.

Но промежуточный трейт надо объявить у себя. Если нужен dyn A + B из чужих трейтов и лень (или нельзя из-за orphan rule) объявлять свой промежуточный, из коробки такое не поедет. Полноценный dyn A + B без обёртки обсуждается давно, но требует довольно жирной переработки раскладки fat pointer, а вместе с ней и слома ABI.

Что нельзя даже с приведением

Vec<Box<dyn Sub>> в Vec<Box<dyn Super>> не работает. Приведение — это coercion, а не subtyping. Coercion применяется в конкретных позициях (аргументы функций, присваивания), но не прокачивается вглубь обобщённых типов. Vec<Box<dyn Sub>> и Vec<Box<dyn Super>> это разные типы, отношения подтипирования между ними нет.

let v: Vec<Box<dyn Sub>> = vec![Box::new(MyType)];
let w: Vec<Box<dyn Super>> = v;  // ошибка

Обход, ручное преобразование через map:

let w: Vec<Box<dyn Super>> = v.into_iter().map(|x| x as Box<dyn Super>).collect();

Работает, но с аллокацией нового вектора. Если бы Vec<Box<dyn Sub>> был подтипом Vec<Box<dyn Super>>, то через изменяемую ссылку на такой Vec можно было бы положить любой Box<dyn Super>, включая тот, который не Sub.

К неродственному трейту привестись нельзя. &dyn Sub в &dyn Unrelated, даже если конкретный тип реализует оба, не работает. В vtable Sub нет ссылки на vtable Unrelated (только на супертрейты Sub), рантайм не знает, откуда достать. Если нужно такое, либо Any плюс downcast, либо специально спроектированный общий супертрейт.

К трейту с совпадающими методами тоже нельзя. impl Read и impl MyRead с идентичными методами это разные вещи. Rust структурно не типизирует трейты, совпадение сигнатур не даёт совместимости.

Из Box<dyn Sub> в &dyn Super напрямую нельзя. Надо сначала взять ссылку: &*box_sub as &dyn Super. Или преобразовать сам Box: let box_super: Box<dyn Super> = box_sub;, вот это работает.

Разные времена жизни могут ловить. &dyn Sub + 'a в &dyn Super + 'b требует 'a: 'b. Стандартные правила времён жизни, но в обобщённом коде иногда ловятся неожиданно.

Трейты с обобщёнными методами по-прежнему не совместимы с dyn. Если у трейта есть обобщённый метод без Self: Sized, из него dyn Trait не построишь, приводить туда нечего. Тут ничего не поменялось.

ptr::metadata: всё ещё nightly

Хотел бы сказать, что вместе с приведением в стабильный Rust выехали и ptr::metadata, ptr::from_raw_parts, DynMetadata. Хотел бы, но нет. Всё это до сих пор сидит в nightly под фичей ptr_metadata. Что довольно странно, потому что API у этой штуки уже несколько лет как готов, и он нужен для написания кастомных умных указателей на dyn Trait.

Что там осталось нерешённого. В основном мелочи вокруг именования: Thin как трейт-алиас против PointeeSized в предикатах, конкретное имя Pointee, где размещать (в core::ptr или в core::marker). Плюс вопрос, нужно ли делать ptr::from_raw_parts небезопасным: сейчас безопасно, но раскрутить его в неправильном виде можно и получить UB.

Пока что если нужен доступ к vtable через типизированный API, есть два варианта. Первый: nightly плюс #![feature(ptr_metadata)]. Второй: crate ptr_meta, стабилизация того же API на стабильном компиляторе через трансмуты.

use ptr_meta::{metadata, from_raw_parts, DynMetadata, Pointee};

fn split_and_recombine(x: &dyn Debug) {
    let data_ptr = x as *const _ as *const ();
    let meta: DynMetadata<dyn Debug> = metadata(x);
    
    println!("size = {}, align = {}", meta.size_of(), meta.align_of());
    
    let reconstructed: *const dyn Debug = from_raw_parts(data_ptr, meta);
}

Для приведения через DynMetadata нужно чуть больше кода (обёртки поверх ptr_meta), но это возможно. Если пишешь свой тип похожий на Rc или контейнер для dyn на основе арены, ptr_meta это то, куда смотреть.

Когда ptr_metadata стабилизируют, откроется намного больше сценариев без unsafe. Пока ждём.

Что дальше

Trait upcasting долго висел флагманской «недостающей фичей» в системе dyn. Теперь не висит, и на месте старой проблемы уже проросли новые хотелки (как это всегда и бывает).

Из того, что обсуждается: полноценный dyn A + B без промежуточного трейта (упирается в переработку fat pointer, вряд ли скоро), нормальные асинхронные трейт-объекты (лечится крейтом async-trait, но хочется в языке), совместимость обобщённых трейтов с dyn, стабилизация ptr::metadata (открытый вопрос уже кучу лет), lint по порядку супертрейтов, чтобы компилятор ругался, когда бесплатную плоскую раскладку разбазаривают. Ничего революционного, просто закрывать оставшиеся углы.

Практический вывод по году такой. Если проектируется новый API — разложить ответственности по маленьким трейтам с супертрейтами, спокойно и без страха, что позже придётся костылять as_super. Если в проекте лежит старый код с ручными as_* — удалить, компилятор сделает всё сам, и vtable похудеет. Если живёте на Any плюс TypeId::downcast_ref — посмотреть в сторону dyn MyAny: Any, поиск в рантайме превращается в проверку на этапе компиляции, а это на порядок надёжнее.

Пользуйтесь и делитесь опытом в комментариях. Спасибо за прочтение статьи!


Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.

Воспользоваться