impl Trait и dyn Trait
Выбор между impl Trait и dyn Trait - ежедневное решение, и его любят спрашивать. Проверяют, знаешь ли ты, что такое трейт-объект физически, сколько он стоит и почему некоторые трейты в него не превращаются.
Стержень: дженерик разворачивается в конкретный тип на этапе компиляции, трейт-объект хранит указатель на данные и таблицу методов и решает всё в рантайме.
// Формулировки: «impl Trait или Box<dyn Trait>?», «сколько занимает &dyn?», «почему трейт не dyn-совместим?»
Что такое &dyn физически
Ссылка на трейт-объект - толстый указатель из двух слов: адрес самих данных и адрес vtable. В таблице лежат адреса методов, размер, выравнивание и деструктор. Проверено: size_of::<&dyn Debug>() равен 16 против 8 у обычной ссылки.
Стоимость вызова - чтение адреса из таблицы и переход, единицы тактов. Настоящая цена в другом: тело метода неизвестно на месте вызова, поэтому нет инлайнинга, а вместе с ним не работают развёртка циклов, векторизация и свёртка констант. В горячем цикле это видно, в обработчике HTTP-запроса теряется на фоне ввода-вывода.
- vtable
- таблица адресов методов, размера и деструктора
- толстый указатель
- два слова: данные плюс vtable или длина
impl Trait скрывает имя, а не тип
fn make() -> impl Shape означает: возвращается один конкретный тип, но называть его я не буду. Поэтому вернуть Circle в одной ветке и Square в другой нельзя - компилятор скажет E0308, if and else have incompatible types.
Когда типы действительно должны различаться - коллекция обработчиков, плагины, разные ошибки, нужен трейт-объект: Box<dyn Shape>. Плата понятна: аллокация и косвенный вызов. Выигрыш - один экземпляр кода вместо копии на каждый тип и возможность держать разнородные значения в одном Vec.
// Практическое правило: начинай со статики, переходи на dyn там, где без разнородности не обойтись или где мономорфизация раздувает сборку.
// разные типы в ветках — только так:
fn make(b: bool) -> Box<dyn Shape> {
if b { Box::new(Circle) }
else { Box::new(Square) }
}- impl Trait
- один неназванный конкретный тип
- E0308
- несовпадение типов в ветках выражения
Когда трейт не годится для dyn
Чтобы построить vtable, компилятор должен знать конечный список методов с фиксированными адресами. Дженерик-метод порождает функцию на каждый T - перечислить их заранее невозможно. То же с ассоциированными константами и методами, возвращающими Self по значению.
Такой трейт называют не dyn-совместимым; раньше говорили object safe, но с 1.83 терминологию сменили, и компилятор пишет «the trait is not dyn compatible» с кодом E0038. Проверено на 1.97 - формулировка именно такая.
// Обход: пометить проблемный метод ограничением where Self: Sized. Тогда он исключается из таблицы, остальной трейт остаётся пригодным для трейт-объекта.
- dyn compatibility
- пригодность трейта для трейт-объекта (бывш. object safety)
- E0038
- трейт нельзя использовать как трейт-объект
Как отвечать: «Когда брать impl Trait, а когда Box<dyn Trait>?»
По умолчанию беру статику: дженерик или impl Trait. Она мономорфизируется, вызовы инлайнятся, накладных расходов нет. Перехожу на Box<dyn Trait>, когда в одной коллекции должны лежать разные реализации - обработчики, плагины, ошибки, или когда мономорфизация начинает раздувать бинарник и время сборки. Стоимость трейт-объекта это аллокация и косвенный вызов через vtable; сам переход дешёвый, теряется в основном инлайнинг и оптимизации вокруг него. И надо помнить, что не всякий трейт годится для dyn: дженерик-методы и ассоциированные константы делают его несовместимым.
Ответ даёт правило по умолчанию, критерий переключения и честную цену. Плюс упоминание dyn-совместимости показывает, что ты натыкался на это в реальном коде.
На чём валятся
- −Думают, что impl Trait в возврате скрывает разные типы. Он скрывает имя одного типа.
- −Считают динамическую диспетчеризацию поиском метода по имени это индекс в таблице.
- −Забывают, что &dyn Trait весит два слова, и удивляются размерам структур с трейт-объектами.
- −Добавляют дженерик-метод в трейт и теряют возможность держать его в Box<dyn>.
- −Говорят «object safe», не зная, что терминология сменилась на dyn compatible.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Когда стоит взять Box<dyn Trait> вместо дженерика с impl Trait?A)Когда в одной коллекции нужны значения разных конкретных типовB)Когда трейт содержит дженерик-методыC)Когда важна скорость: динамический вызов дешевле мономорфизацииD)Когда типы известны на этапе компиляции и их немного
показать ответ и разбор
+A)Когда в одной коллекции нужны значения разных конкретных типов// разбор: Дженерик разворачивается в конкретный тип на каждый вызов — Vec<T> хранит однородные значения. Если в списке должны лежать разные реализации (обработчики, плагины, ошибки), нужен трейт-объект: Vec<Box<dyn Handler>>. Плата — динамические вызовы и потеря инлайнинга, зато меньше копий кода и быстрее сборка.
- Функция объявлена как fn make(b: bool) -> impl Shape и возвращает Circle в одной ветке и Square в другой. Что скажет компилятор?A)Соберёт: impl Trait для того и нужен, чтобы скрыть разные типыB)Соберёт с предупреждением о неявном приведении к трейт-объектуC)Ошибку: у impl Trait один конкретный тип, ветки несовместимыD)Ошибку: impl Trait запрещён в возвращаемой позиции
показать ответ и разбор
+C)Ошибку: у impl Trait один конкретный тип, ветки несовместимы// разбор: impl Trait в возврате означает «конкретный тип, который я не называю» — он один и тот же для всех путей выполнения. Разные типы в ветках дают E0308: if and else have incompatible types. Когда тип действительно должен различаться, возвращают Box<dyn Shape> и платят динамической диспетчеризацией.
- Трейт содержит fn parse<T: FromStr>(&self, s: &str) -> T. Почему &dyn этого трейта не собирается?A)Метод возвращает T по значению, а трейт-объект работает только со ссылкамиB)Дженерик-методы требуют, чтобы трейт был помечен как unsafeC)FromStr не реализован для всех типов, поэтому таблица методов неполнаD)Трейт не dyn-совместим: под дженерик-метод нет одной записи в vtable
показать ответ и разбор
+D)Трейт не dyn-совместим: под дженерик-метод нет одной записи в vtable// разбор: vtable — таблица конкретных адресов, а дженерик-метод порождает по функции на каждый T: заранее их не перечислить. Такой трейт компилятор считает не dyn-совместимым (раньше говорили object safe) и роняет сборку с E0038. Лечится либо выносом метода за трейт, либо ограничением where Self: Sized, которое исключает его из таблицы.
- Насколько дорога динамическая диспетчеризация по сравнению со статической?A)Дороже в разы: каждый вызов ищет метод по имени в таблицеB)Сам вызов дёшев, теряются инлайнинг и оптимизации вокругC)Одинаково: компилятор разворачивает dyn в статические вызовыD)Дороже только при первом вызове, дальше адрес кэшируется
показать ответ и разбор
+B)Сам вызов дёшев, теряются инлайнинг и оптимизации вокруг// разбор: Косвенный вызов через vtable — это чтение адреса и переход, единицы тактов. Настоящая цена в другом: тело метода неизвестно на месте вызова, поэтому нет инлайнинга, а с ним не работают развёртка циклов, векторизация и свёртка констант. В горячем цикле разница заметна, в обработчике запроса — теряется на фоне ввода-вывода.
- Что называют статической диспетчеризацией?A)Разрешение имён методов по порядку подключённых модулей в текущей области видимостиB)Выбор реализации по таблице методов, которую компилятор строит один раз для всех типов сразуC)Выбор реализации на этапе компиляции: адрес метода известен и вызов можно встроитьD)Кэширование адреса метода после первого вызова, чтобы дальше идти по короткому пути
показать ответ и разбор
+C)Выбор реализации на этапе компиляции: адрес метода известен и вызов можно встроить// разбор: Дженерик разворачивается под конкретный тип, поэтому вызываемая функция известна прямо в точке вызова: компилятор подставляет адрес, часто встраивает тело и оптимизирует всё вокруг. Это и есть zero-cost abstraction — абстракция обошлась в ноль инструкций. Противоположность — динамическая диспетчеризация через dyn, где адрес читается из таблицы в рантайме.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.