Вопросы по основам Rust на собеседовании
Базовые вопросы тут не формальность: почти каждый проверяет, не притащил ли кандидат привычки из другого языка. Строка не индексируется по символам, целое переполнение ведёт себя по-разному в debug и release, а let можно объявить дважды — и это не ошибка, а затенение.
Что спрашивают
- +Типы и переменные: вывод типа, mut против повторного let, константы и static, приведения as и их потери
- +Числа: переполнение в debug и release, checked, saturating и wrapping — какой вариант когда честнее
- +Строки: String и &str, почему нет индексации по байтам, chars против bytes, работа с UTF-8
- +Коллекции: Vec, HashMap, ленивые итераторы, collect и turbofish, чем итератор дешевле цикла с индексом
- +match и Option: исчерпывающность, if let и let else, unwrap_or против unwrap_or_else, отсутствие null
- +Модули: дерево mod, pub и pub(crate), пути use и структура крейта из нескольких файлов
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, что просели.
- Коллекции и итераторы13
- Типы, let и mut13
- match и Option12
- Модули и видимость11
- Строки: String и &str11
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Типы, let и mut в Rust13 вопросов
- match и Option в Rust12 вопросов
- Модули и видимость в Rust11 вопросов
Примеры вопросов с разбором
- Vec создан через with_capacity(2), в него положили два элемента и добавляют третий. Что произойдёт?A)Выделится новый буфер большего размера, данные переедут в негоB)Третий элемент запишется за границей буфера — это UBC)push вернёт ошибку: ёмкость исчерпанаD)Vec допишет элемент в тот же буфер: ёмкость лишь подсказка аллокатору
показать ответ и разбор
+A)Выделится новый буфер большего размера, данные переедут в него// разбор: При нехватке места Vec выделяет новый буфер (обычно вдвое больше), перемещает туда элементы и освобождает старый — амортизированно push остаётся O(1). Из этого следуют два практических вывода: указатели и ссылки на элементы после роста невалидны (borrow checker это и запрещает), а with_capacity экономит перевыделения, когда размер известен заранее.
- Почему компилятор требует покрыть все варианты в match?A)Чтобы оптимизатор построил таблицу переходов вместо цепочки ifB)Чтобы непокрытый случай нельзя было забыть: это ошибка компиляцииC)Это требование только для enum из стандартной библиотекиD)Чтобы ветки match сходились к одному типу значения
показать ответ и разбор
+B)Чтобы непокрытый случай нельзя было забыть: это ошибка компиляции// разбор: Исчерпывающий разбор — способ поймать забытый случай на этапе компиляции: добавили вариант в enum, и все match без ветки для него перестают собираться. Отсюда практика избегать ветку _ в разборе своих enum — она глушит именно ту диагностику, ради которой всё и делается.
- Какая видимость у элементов модуля по умолчанию?A)Приватная: видно только внутри модуля и его потомковB)Публичная: pub нужен лишь для экспорта из библиотекиC)Публичная внутри крейта, приватная снаружиD)Зависит от места объявления: в lib.rs — публичная, в остальных — приватная
показать ответ и разбор
+A)Приватная: видно только внутри модуля и его потомков// разбор: Всё приватно, пока не написан pub. Дочерний модуль видит элементы родителя, обратное не работает: родителю нужен pub у потомка. Это и делает модуль настоящей границей инкапсуляции — внутренности не утекают, пока автор явно не решит их показать.
- В чём разница между String и &str?A)String владеет буфером в куче и растёт, &str — заимствованный срез UTF-8B)String — тип стандартной библиотеки, &str — устаревший тип из ранних версийC)String хранит UTF-8, &str — ASCII-строку фиксированной длиныD)String лежит в куче, &str — только на литерал в бинарнике
показать ответ и разбор
+A)String владеет буфером в куче и растёт, &str — заимствованный срез UTF-8// разбор: String — владеющий, изменяемый буфер в куче (указатель, длина, ёмкость). &str — невладеющий вид на кусок UTF-8: указатель и длина, без ёмкости. Отсюда правило API: аргументы принимают &str (подойдёт и литерал, и срез String), а возвращают String, когда нужно отдать владение.
- Чем shadowing (let x = ...; let x = ...) отличается от объявления let mut x?A)Shadowing заводит новую переменную, mut меняет ту же ячейкуB)Это синонимы: компилятор разворачивает shadowing в mutC)Shadowing работает только внутри циклов, mut — вездеD)Shadowing меняет значение, но запрещает менять тип переменной
показать ответ и разбор
+A)Shadowing заводит новую переменную, mut меняет ту же ячейку// разбор: Второй let создаёт новую переменную с тем же именем: старая остаётся жить до конца области видимости, но недоступна по имени. Отсюда главное практическое отличие — при shadowing можно сменить тип (let s = "5"; let s: i32 = s.parse().unwrap()), а mut требует того же типа, зато позволяет менять значение в той же ячейке.
- Считаем частоты слов в HashMap<String, i32>. Что делает *map.entry(w).or_insert(0) += 1?A)Перезаписывает значение нулём, затем прибавляет единицуB)Вставляет 0, если ключа нет, и увеличивает значение по ссылкеC)Возвращает копию значения, инкремент в карту не попадаетD)Паникует, если ключ уже есть: or_insert вставляет только новые
показать ответ и разбор
+B)Вставляет 0, если ключа нет, и увеличивает значение по ссылке// разбор: entry возвращает вход в карту, or_insert кладёт значение по умолчанию только при его отсутствии и в любом случае отдаёт &mut на значение — разыменование и += меняют его на месте. Это идиома «посмотреть и обновить за один поиск»: get + insert искали бы ключ дважды.
- Что Option<T> даёт по сравнению с nullable-ссылкой из других языков?A)Option быстрее в рантайме: проверка на null исчезаетB)Option разрешает вызывать методы T напрямую, подставляя значение по умолчаниюC)Option — это указатель с проверкой на ноль в рантаймеD)Отсутствие значения видно в типе, и компилятор требует его разобрать
показать ответ и разбор
+D)Отсутствие значения видно в типе, и компилятор требует его разобрать// разбор: Пустоту нельзя не заметить: она часть типа, и добраться до значения без разбора (match, if let, комбинаторы) не выйдет. Дырка «забыл проверить на null» закрывается на этапе компиляции. При этом Option<&T> и Option<Box<T>> благодаря niche-оптимизации занимают столько же, сколько сам указатель.
- Что означают crate::, self:: и super:: в путях?A)Внешнюю зависимость, текущий файл и стандартную библиотекуB)Корень текущего крейта, текущий модуль и родительский модульC)Публичный API, приватную часть и переэкспортD)Корень воркспейса, текущий крейт и родительский крейт
показать ответ и разбор
+B)Корень текущего крейта, текущий модуль и родительский модуль// разбор: Пути бывают абсолютные (от crate:: или имени внешнего крейта) и относительные (self::, super:: или просто имя элемента). super:: полезен в тестовом подмодуле — там пишут use super::*, чтобы затащить проверяемый код. Модули при этом не обязаны совпадать с файлами: mod задаёт дерево, файлы лишь удобная раскладка.
- Чему равно "привет".len()?A)6 — len возвращает количество символовB)7 — шесть символов плюс завершающий нульC)24 — каждый символ хранится как char по 4 байтаD)12 — len считает байты UTF-8, кириллица занимает по два
показать ответ и разбор
+D)12 — len считает байты UTF-8, кириллица занимает по два// разбор: len у str и String — длина в байтах, а не в символах: строки хранятся как UTF-8, где кириллическая буква занимает два байта. Символы считают через chars().count(), но это O(n) и всё равно не то же самое, что «видимые знаки»: e с комбинирующим акутом даст два char при одном отображаемом символе.
это 9 из 60
Ещё 51 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы приходят сессиями, а подтему, на которой ты споткнулся, движок принесёт снова: завтра, через три дня, через неделю. Бесплатно, с дневным лимитом вопросов.
Частые вопросы
Чем mut отличается от повторного let?
mut разрешает менять то же самое значение по тому же адресу, а повторный let создаёт новую переменную, затеняющую старую, — можно даже с другим типом. Затенение обычно используют, чтобы преобразовать значение и переиспользовать имя, не заводя mut.
Что происходит при переполнении целого?
В отладочной сборке это паника, в релизной — обёртывание по модулю. Именно поэтому в коде, где переполнение возможно, берут явные методы: checked_add вернёт Option, saturating_add упрётся в границу, wrapping_add обернётся осознанно.
Почему нельзя обратиться к символу строки по индексу?
String хранит UTF-8, где символ занимает от одного до четырёх байт, поэтому индекс по байтам мог бы разрезать символ пополам. Rust не даёт такой операции вообще: нужен обход через chars или срез по границам символов.