Вопросы по владению и лайфтаймам Rust на собеседовании
Владение — входной фильтр Rust-собеса: тут отсеивают тех, кто читал книгу, но не спорил с borrow checker. На трёх правилах разговор не останавливается: дальше идут частичный move, NLL и вопрос, почему два узла, ссылающиеся друг на друга через Rc, не освобождаются никогда.
Что спрашивают
- +Владение и move: что происходит при передаче String в функцию, почему i32 копируется, частичный move у структуры
- +Заимствование: правило XOR, ошибки E0499 и E0502, NLL и переподзаимствование, split_at_mut
- +Лайфтаймы: зачем аннотация, правила elision, два смысла 'static, ссылка на локальное значение
- +Умные указатели: Box для кучи и рекурсии, Rc против Arc, Weak против циклов
- +Внутренняя изменяемость: Cell и RefCell, паника вместо ошибки компиляции, почему RefCell не Sync
- +Drop и RAII: порядок уничтожения, запрет явного вызова деструктора, mem::forget и утечки
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- Box, Rc, Arc12
- Drop и RAII12
- Interior mutability12
- Владение и move12
- Заимствование и ссылки12
- Лайфтаймы и elision12
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Владение и перемещение в Rust12 вопросов
- Заимствование и правило одной изменяемой ссылки12 вопросов
- Лайфтаймы в Rust12 вопросов
- Умные указатели в Rust: Box, Rc, Arc12 вопросов
- Interior mutability в Rust12 вопросов
- Drop и RAII в Rust12 вопросов
Примеры вопросов с разбором
- Сколько ссылок на одно значение допускает borrow checker одновременно?A)Одну & и одну &mut: чтение и запись не мешают друг другуB)Либо сколько угодно &, либо ровно одна &mutC)По одной ссылке каждого вида на область видимостиD)Сколько угодно ссылок обоих видов: проверки для потоков
показать ответ и разбор
+B)Либо сколько угодно &, либо ровно одна &mut// разбор: Правило XOR: разделяемое чтение или эксклюзивная запись, но не вместе. Оно исключает целый класс багов — изменение коллекции во время итерации, висячие указатели после перевыделения буфера, гонки данных. Проверка статическая: нарушение не доходит до рантайма, компилятор роняет сборку с E0499 или E0502.
- Когда вызывается деструктор значения в Rust?A)Когда сборщик мусора решит, что значение недостижимоB)Когда владелец выходит из области видимости или перезаписываетсяC)В конце функции, где значение было создано, независимо от владенияD)Только при явном вызове drop — иначе память освободит ОС
показать ответ и разбор
+B)Когда владелец выходит из области видимости или перезаписывается// разбор: Это RAII: освобождение привязано к владению. Область видимости кончилась — компилятор вставил вызов drop; значение перемещено — освобождать будет новый владелец; переменной присвоили новое значение — старое уничтожается. Никакого сборщика мусора и никаких пауз: моменты освобождения известны на этапе компиляции.
- Что такое внутренняя изменяемость (interior mutability)?A)Изменение значения через общую ссылку &T под контролем типа-обёрткиB)Возможность объявить поле структуры mut отдельно от самой структурыC)Автоматическое превращение & в &mut, когда других ссылок нетD)Изменение данных из другого потока без синхронизации
показать ответ и разбор
+A)Изменение значения через общую ссылку &T под контролем типа-обёртки// разбор: Обычно &T запрещает запись. Cell, RefCell, Mutex и атомики дают легальный обход: снаружи ссылка общая, а обёртка сама следит, чтобы одновременных изменений не случилось — по значению (Cell), по счётчикам в рантайме (RefCell) или блокировкой (Mutex). Отдельного модификатора mut у полей в Rust нет.
- Что делает аннотация лайфтайма 'a в сигнатуре функции?A)Продлевает жизнь значения до конца программыB)Заставляет компилятор освободить память сразу после вызоваC)Указывает, сколько тактов значение остаётся в кэшеD)Описывает связь между временами жизни аргументов и результата
показать ответ и разбор
+D)Описывает связь между временами жизни аргументов и результата// разбор: Лайфтайм — это не рычаг управления памятью, а описание отношений: «результат живёт не дольше, чем вот этот аргумент». Компилятор проверяет, что вызывающий код это отношение соблюдает. На сгенерированный машинный код аннотации не влияют — они стираются после проверки.
- Что происходит с переменной s: String после вызова take(s), где fn take(v: String)?A)s копируется в аргумент, оригинал остаётся доступнымB)Передаётся ссылка, s остаётся владельцем буфераC)Владение уезжает в функцию, дальше s использовать не дадутD)s обнуляется в рантайме и становится пустой строкой
показать ответ и разбор
+C)Владение уезжает в функцию, дальше s использовать не дадут// разбор: Аргумент типа String принимается по значению — владение переходит вызываемой функции, и она отвечает за освобождение буфера. Компилятор помечает s как перемещённую: любое обращение после вызова — ошибка E0382. Если оригинал нужен дальше, передают ссылку (&s) или явную копию (s.clone()).
- Зачем нужен Box<T>, если значение и так можно положить в переменную?A)Он делает значение неизменяемым и разделяемым между потокамиB)Он считает ссылки и освобождает память, когда их не осталосьC)Кладёт значение в кучу: нужен рекурсии и dyn TraitD)Он ускоряет доступ: куча выделяется быстрее стека
показать ответ и разбор
+C)Кладёт значение в кучу: нужен рекурсии и dyn Trait// разбор: Box — владеющий указатель на кучу с нулевым оверхедом сверх самого указателя. Он нужен там, где размер на этапе компиляции неизвестен или бесконечен: рекурсивный enum вроде списка, трейт-объект Box<dyn Error>, большое значение, которое дорого двигать по стеку. Освобождение — по выходу владельца из области видимости.
- Почему этот код не собирается?
let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); println!("{}", first);A)Вектор запрещено менять после того, как его прочиталиB)first указывает на копию, а печатать копию после push запрещеноC)Ошибка в println: ссылку нужно разыменовать через *firstD)push требует &mut, пока жива ссылка first — E0502показать ответ и разбор
+D)push требует &mut, пока жива ссылка first — E0502// разбор: first — неизменяемое заимствование вектора, а push берёт &mut. Это прямое нарушение XOR-правила, и запрет не формальность: push может перевыделить буфер, и старый указатель повис бы. Уберите последний println — заимствование закончится раньше push, и код соберётся.
- Что напечатает код?
struct D(&'static str); impl Drop for D { fn drop(&mut self) { println!("drop {}", self.0); } } let _a = D("a"); let _b = D("b"); println!("конец");A)конец, drop b, drop aB)drop a, drop b, конецC)конец, drop a, drop bD)конец — деструкторы для переменных с подчёркиванием не зовутсяпоказать ответ и разбор
+A)конец, drop b, drop a// разбор: Локальные переменные уничтожаются в порядке, обратном объявлению: стек разбирается сверху вниз, поэтому сначала b, потом a. Подчёркивание в начале имени лишь глушит предупреждение о неиспользовании. А вот голое имя _ — другое дело: оно вообще не создаёт привязку, и значение уничтожается сразу.
- Когда берут Cell<T>, а когда RefCell<T>?A)Cell — для чтения, RefCell — для записиB)Cell — для Copy-значений целиком, RefCell — когда нужны ссылки внутрьC)Cell — для примитивов, RefCell — для типов из стандартной библиотекиD)Cell — для однопоточного кода, RefCell — для многопоточного
показать ответ и разбор
+B)Cell — для Copy-значений целиком, RefCell — когда нужны ссылки внутрь// разбор: Cell работает только целыми значениями: get копирует, set заменяет — ссылок внутрь он не выдаёт, поэтому и проверять нечего, накладных расходов ноль. RefCell выдаёт настоящие ссылки (borrow, borrow_mut) и потому ведёт счётчики заимствований в рантайме, платя проверками и риском паники.
это 9 из 72
Ещё 63 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Что происходит при передаче String в функцию?
Владение уезжает вместе со значением: копируется заголовок из указателя, длины и ёмкости, буфер в куче остаётся на месте. Старое имя компилятор помечает перемещённым, и обращение к нему после вызова даёт ошибку E0382.
Зачем нужны лайфтаймы, если память освобождается сама?
Лайфтайм ничего не продлевает: это описание связи между временами жизни аргументов и результата, которое проверяет компилятор. Благодаря ему нельзя вернуть ссылку на локальное значение, а висячих указателей не существует как класса ошибок.
Почему RefCell роняет программу, а не сборку?
Он переносит правило XOR из компиляции в рантайм: счётчик заимствований живёт в самом значении. Второй borrow_mut при живом первом паникует — это цена гибкости, и потому RefCell берут только там, где иначе не выразить.