сеньорчикОткрыть в Telegram
← все вопросывопросы для собеседований · Трейты и дженерики

Вопросы по трейтам и дженерикам Rust на собеседовании

Здесь проверяют, понимаешь ли ты цену абстракции: дженерик разворачивается в отдельную копию кода на каждый тип, трейт-объект платит таблицей методов, а замыкание — это структура с захваченным окружением. Любимый вопрос — почему трейт с дженерик-методом нельзя положить в Box.

84 вопросов в банке·7 подтем·ниже разбор 9

Что спрашивают

Из чего состоит тема

Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.

Разборы подтем

Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.

Примеры вопросов с разбором

  1. #rs_assoc_types1 / 9
    Почему у Iterator тип элемента объявлен как type Item, а не как параметр Iterator<T>?
    A)Тип элемента определяется реализацией: у одного типа он ровно один
    B)Параметры типов у трейтов появились позже и пока не рекомендуются
    C)Ассоциированные типы быстрее: они не участвуют в мономорфизации
    D)Так короче писать: параметр пришлось бы указывать в каждом вызове
    показать ответ и разбор
    +A)Тип элемента определяется реализацией: у одного типа он ровно один

    // разбор: Ассоциированный тип — часть контракта реализации, а не выбор вызывающего: вектор строк итерируется строками, и второго варианта быть не может. Поэтому Iterator реализуется для типа один раз, а Item выводится сам. Параметр же допускает несколько реализаций — так устроен From<T>, который реализуют многократно.

  2. #rs_closures2 / 9
    Чем замыкание отличается от обычной функции?
    A)Замыкание размещается в куче, функция — на стеке
    B)Замыкание живёт только в той функции, где объявлено
    C)Замыкание захватывает переменные из окружения, функция — нет
    D)У замыкания нет типа: компилятор вставляет тело в место вызова
    показать ответ и разбор
    +C)Замыкание захватывает переменные из окружения, функция — нет

    // разбор: Замыкание — это анонимная функция плюс захваченное окружение: компилятор строит под неё структуру с захваченными значениями и реализует для неё один из трёх трейтов вызова. Обычная fn о месте объявления не помнит ничего, поэтому её адрес умещается в указатель. Захват бывает по ссылке, по изменяемой ссылке или по владению — способ выбирает компилятор, а move заставляет брать владение.

  3. #rs_coherence3 / 9
    Почему нельзя реализовать 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 пишется уже для своего типа.

  4. #rs_dispatch4 / 9
    Чем &dyn Trait отличается от обычной ссылки на конкретный тип в памяти?
    A)Это указатель на объект-обёртку, которая хранит тип и значение
    B)Это жирный указатель: адрес данных плюс адрес таблицы методов
    C)Это обычный указатель, тип восстанавливается по заголовку значения
    D)Это индекс в глобальной таблице типов программы
    показать ответ и разбор
    +B)Это жирный указатель: адрес данных плюс адрес таблицы методов

    // разбор: Ссылка на трейт-объект занимает два слова (16 байт на 64-битной платформе): данные и vtable с адресами методов, размером и деструктором. Отсюда и цена динамической диспетчеризации — лишний уровень косвенности и потеря инлайнинга. Заголовков типа у значений нет, всё решает вторая половина указателя.

  5. #rs_generics5 / 9
    Что делает компилятор с fn max<T: PartialOrd>(a: T, b: T) при вызовах для i32 и для String?
    A)Собирает одну функцию и передаёт таблицу методов скрытым аргументом
    B)Генерирует отдельную специализированную функцию под каждый тип
    C)Собирает одну функцию, которая читает тип из заголовка значения
    D)Откладывает выбор до рантайма и кэширует результат для типа
    показать ответ и разбор
    +B)Генерирует отдельную специализированную функцию под каждый тип

    // разбор: Это мономорфизация: под каждый использованный набор типов создаётся своя копия кода с прямыми вызовами, поэтому дженерики в Rust не стоят ничего в рантайме и хорошо инлайнятся. Плата — время компиляции и размер бинарника; когда копий становится слишком много, часть кода прячут за dyn.

  6. #rs_structs_enums6 / 9
    Чем enum в Rust отличается от перечисления в C или Java?
    A)Варианты нумеруются автоматически и приводятся к int через as
    B)Каждый вариант может нести свои данные разных типов
    C)Варианты — это отдельные типы, между собой не связанные
    D)Enum хранится в куче, поэтому вариантов может быть сколько угодно
    показать ответ и разбор
    +B)Каждый вариант может нести свои данные разных типов

    // разбор: Это размеченное объединение: Message::Quit без данных, Message::Move { x, y } со структурой, Message::Write(String) с полезной нагрузкой — всё в одном типе. Отсюда Option и Result: они не встроены в язык, а объявлены обычным enum. Разбирают такой тип через match, и компилятор требует покрыть все варианты.

  7. #rs_traits7 / 9
    Что такое трейт и чем он отличается от класса в ООП-языках?
    A)Базовый класс, от которого структуры наследуют поля и методы
    B)Пространство имён для функций, работающих с одним типом
    C)Набор поведения без данных: типы реализуют его, не наследуют
    D)Шаблон структуры, из которого компилятор порождает конкретные типы
    показать ответ и разбор
    +C)Набор поведения без данных: типы реализуют его, не наследуют

    // разбор: Трейт описывает, что тип умеет: сигнатуры методов, при желании с реализацией по умолчанию. Полей у трейта нет, наследования состояния тоже — тип объявляет соответствие отдельным impl-блоком, в том числе для чужого типа из другого крейта. Это ближе к интерфейсам и тайпклассам, чем к классам.

  8. #rs_assoc_types8 / 9
    Что произойдёт при попытке реализовать Iterator для одного типа дважды — с Item = i32 и Item = u8?
    A)Соберётся: реализации различаются ассоциированным типом
    B)Соберётся, но next() будет выбираться по контексту вызова
    C)Ошибка E0119: конфликтующие реализации трейта для типа
    D)Ошибка: у Iterator запрещено переопределять Item вручную
    показать ответ и разбор
    +C)Ошибка E0119: конфликтующие реализации трейта для типа

    // разбор: Ассоциированный тип не входит в идентичность реализации: трейт без параметров реализуется для типа ровно один раз, поэтому второй impl конфликтует с первым. Если действительно нужны две «версии» поведения, берут либо трейт с параметром (T<X>), либо разные типы-обёртки поверх одного значения.

  9. #rs_closures9 / 9
    Чем различаются трейты Fn, FnMut и FnOnce?
    A)Числом аргументов, которое принимает замыкание
    B)Тем, как замыкание обращается с захваченным: читает, меняет, потребляет
    C)Местом хранения замыкания: стек, куча, статическая память
    D)Тем, сколько потоков могут вызвать замыкание одновременно
    показать ответ и разбор
    +B)Тем, как замыкание обращается с захваченным: читает, меняет, потребляет

    // разбор: FnOnce берёт self по значению — вызвать можно один раз, потому что захваченное потребляется. FnMut берёт &mut self и может менять захваченное между вызовами. Fn берёт &self и только читает. Это вложенная иерархия: всякое Fn годится и как FnMut, и как FnOnce, поэтому в сигнатуре пишут самое слабое требование из достаточных — так подойдёт больше замыканий.

это 9 из 84

Ещё 75 вопросов по теме — в тренажёре, с движком повторения

Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.

Частые вопросы