Вопросы по основам 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 или срез по границам символов.