Вопросы по трейтам и дженерикам Rust на собеседовании
Здесь проверяют, понимаешь ли ты цену абстракции: дженерик разворачивается в отдельную копию кода на каждый тип, трейт-объект платит таблицей методов, а замыкание — это структура с захваченным окружением. Любимый вопрос — почему трейт с дженерик-методом нельзя положить в Box.
Что спрашивают
- +Трейты: поведение без состояния, методы по умолчанию, супертрейты, sealed-приём
- +Дженерики: мономорфизация и её цена, границы и where, турбофиш, грабля derive с лишним ограничением
- +Диспетчеризация: impl Trait против dyn Trait, жирный указатель, dyn-совместимость и E0038
- +Замыкания: Fn, FnMut и FnOnce, move и захват, возврат замыкания через impl Fn или Box<dyn Fn>
- +Ассоциированные типы: почему у Iterator это type Item, GAT и уточнение Item = String
- +Согласованность: orphan rule, newtype как обход, blanket impl и отсутствие специализации
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- impl Trait и dyn Trait12
- Orphan rule и newtype12
- Ассоциированные типы12
- Дженерики и bounds12
- Замыкания: Fn, FnMut, FnOnce12
- Структуры и enum12
- Трейты и реализации12
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Структуры и enum в Rust12 вопросов
- Трейты в Rust12 вопросов
- Замыкания в Rust: Fn, FnMut и FnOnce12 вопросов
- Дженерики в Rust12 вопросов
- impl Trait и dyn Trait12 вопросов
- Ассоциированные типы в Rust12 вопросов
- Orphan rule и newtype в Rust12 вопросов
Примеры вопросов с разбором
- Почему у Iterator тип элемента объявлен как type Item, а не как параметр Iterator<T>?A)Тип элемента определяется реализацией: у одного типа он ровно одинB)Параметры типов у трейтов появились позже и пока не рекомендуютсяC)Ассоциированные типы быстрее: они не участвуют в мономорфизацииD)Так короче писать: параметр пришлось бы указывать в каждом вызове
показать ответ и разбор
+A)Тип элемента определяется реализацией: у одного типа он ровно один// разбор: Ассоциированный тип — часть контракта реализации, а не выбор вызывающего: вектор строк итерируется строками, и второго варианта быть не может. Поэтому Iterator реализуется для типа один раз, а Item выводится сам. Параметр же допускает несколько реализаций — так устроен From<T>, который реализуют многократно.
- Чем замыкание отличается от обычной функции?A)Замыкание размещается в куче, функция — на стекеB)Замыкание живёт только в той функции, где объявленоC)Замыкание захватывает переменные из окружения, функция — нетD)У замыкания нет типа: компилятор вставляет тело в место вызова
показать ответ и разбор
+C)Замыкание захватывает переменные из окружения, функция — нет// разбор: Замыкание — это анонимная функция плюс захваченное окружение: компилятор строит под неё структуру с захваченными значениями и реализует для неё один из трёх трейтов вызова. Обычная fn о месте объявления не помнит ничего, поэтому её адрес умещается в указатель. Захват бывает по ссылке, по изменяемой ссылке или по владению — способ выбирает компилятор, а move заставляет брать владение.
- Почему нельзя реализовать Display для Vec<u8> в своём крейте?A)Display закрыт для реализации извне стандартной библиотекиB)Vec<u8> — дженерик-тип, а для них внешние impl запрещеныC)Orphan rule: и трейт, и тип чужие — impl неоднозначенD)Такой impl требует unsafe и явного разрешения в Cargo.toml
показать ответ и разбор
+C)Orphan rule: и трейт, и тип чужие — impl неоднозначен// разбор: Правило сиротства: хотя бы одна сторона — трейт или тип — должна принадлежать твоему крейту. Иначе два разных крейта дали бы Vec<u8> разные Display, и при сборке вместе выбор стал бы неразрешимым. Обход — newtype: struct Hex(Vec<u8>), и Display пишется уже для своего типа.
- Чем &dyn Trait отличается от обычной ссылки на конкретный тип в памяти?A)Это указатель на объект-обёртку, которая хранит тип и значениеB)Это жирный указатель: адрес данных плюс адрес таблицы методовC)Это обычный указатель, тип восстанавливается по заголовку значенияD)Это индекс в глобальной таблице типов программы
показать ответ и разбор
+B)Это жирный указатель: адрес данных плюс адрес таблицы методов// разбор: Ссылка на трейт-объект занимает два слова (16 байт на 64-битной платформе): данные и vtable с адресами методов, размером и деструктором. Отсюда и цена динамической диспетчеризации — лишний уровень косвенности и потеря инлайнинга. Заголовков типа у значений нет, всё решает вторая половина указателя.
- Что делает компилятор с fn max<T: PartialOrd>(a: T, b: T) при вызовах для i32 и для String?A)Собирает одну функцию и передаёт таблицу методов скрытым аргументомB)Генерирует отдельную специализированную функцию под каждый типC)Собирает одну функцию, которая читает тип из заголовка значенияD)Откладывает выбор до рантайма и кэширует результат для типа
показать ответ и разбор
+B)Генерирует отдельную специализированную функцию под каждый тип// разбор: Это мономорфизация: под каждый использованный набор типов создаётся своя копия кода с прямыми вызовами, поэтому дженерики в Rust не стоят ничего в рантайме и хорошо инлайнятся. Плата — время компиляции и размер бинарника; когда копий становится слишком много, часть кода прячут за dyn.
- Чем enum в Rust отличается от перечисления в C или Java?A)Варианты нумеруются автоматически и приводятся к int через asB)Каждый вариант может нести свои данные разных типовC)Варианты — это отдельные типы, между собой не связанныеD)Enum хранится в куче, поэтому вариантов может быть сколько угодно
показать ответ и разбор
+B)Каждый вариант может нести свои данные разных типов// разбор: Это размеченное объединение: Message::Quit без данных, Message::Move { x, y } со структурой, Message::Write(String) с полезной нагрузкой — всё в одном типе. Отсюда Option и Result: они не встроены в язык, а объявлены обычным enum. Разбирают такой тип через match, и компилятор требует покрыть все варианты.
- Что такое трейт и чем он отличается от класса в ООП-языках?A)Базовый класс, от которого структуры наследуют поля и методыB)Пространство имён для функций, работающих с одним типомC)Набор поведения без данных: типы реализуют его, не наследуютD)Шаблон структуры, из которого компилятор порождает конкретные типы
показать ответ и разбор
+C)Набор поведения без данных: типы реализуют его, не наследуют// разбор: Трейт описывает, что тип умеет: сигнатуры методов, при желании с реализацией по умолчанию. Полей у трейта нет, наследования состояния тоже — тип объявляет соответствие отдельным impl-блоком, в том числе для чужого типа из другого крейта. Это ближе к интерфейсам и тайпклассам, чем к классам.
- Что произойдёт при попытке реализовать Iterator для одного типа дважды — с Item = i32 и Item = u8?A)Соберётся: реализации различаются ассоциированным типомB)Соберётся, но next() будет выбираться по контексту вызоваC)Ошибка E0119: конфликтующие реализации трейта для типаD)Ошибка: у Iterator запрещено переопределять Item вручную
показать ответ и разбор
+C)Ошибка E0119: конфликтующие реализации трейта для типа// разбор: Ассоциированный тип не входит в идентичность реализации: трейт без параметров реализуется для типа ровно один раз, поэтому второй impl конфликтует с первым. Если действительно нужны две «версии» поведения, берут либо трейт с параметром (T<X>), либо разные типы-обёртки поверх одного значения.
- Чем различаются трейты Fn, FnMut и FnOnce?A)Числом аргументов, которое принимает замыканиеB)Тем, как замыкание обращается с захваченным: читает, меняет, потребляетC)Местом хранения замыкания: стек, куча, статическая памятьD)Тем, сколько потоков могут вызвать замыкание одновременно
показать ответ и разбор
+B)Тем, как замыкание обращается с захваченным: читает, меняет, потребляет// разбор: FnOnce берёт self по значению — вызвать можно один раз, потому что захваченное потребляется. FnMut берёт &mut self и может менять захваченное между вызовами. Fn берёт &self и только читает. Это вложенная иерархия: всякое Fn годится и как FnMut, и как FnOnce, поэтому в сигнатуре пишут самое слабое требование из достаточных — так подойдёт больше замыканий.
это 9 из 84
Ещё 75 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Чем impl Trait отличается от dyn Trait?
impl Trait — один конкретный тип, который вы не называете: вызовы статические, аллокаций нет, инлайнинг работает. dyn Trait — трейт-объект с таблицей методов: в одну коллекцию ложатся разные реализации, но вызов идёт через косвенность.
Почему трейт бывает не dyn-совместимым?
В таблицу методов нужно положить конкретные адреса. Дженерик-метод порождает по функции на каждый тип, и заранее их не перечислить, а метод, возвращающий Self по значению, не имеет известного размера — такие трейты компилятор отвергает с E0038.
В чём разница между Fn, FnMut и FnOnce?
В том, как замыкание обращается с захваченным: Fn читает через общую ссылку, FnMut меняет через изменяемую, FnOnce потребляет захваченное и вызывается один раз. Иерархия вложенная, поэтому в сигнатуре пишут самую слабую границу из достаточных.