Правила заимствования и почему нельзя две &mut
Заимствование — способ дать доступ к данным, не отдавая владение; правил всего два, и держатся на них потоки, итераторы и оптимизации.
Borrowing (заимствование) — получение ссылки на значение без передачи владения:
&Tдля чтения и&mut Tдля изменения.
Вопрос: «Сформулируй правила заимствования»
Вопрос-фильтр. Формулировка должна отскакивать от зубов, потому что дальше интервьюер будет строить на ней всё остальное.
Что на самом деле проверяет интервьюер
Что ты знаешь их именно как «ИЛИ», а не как список ограничений. Правильно так: в один момент времени для значения может существовать либо одна изменяемая ссылка &mut T, либо сколько угодно общих ссылок &T, но не то и другое сразу. Плюс третье, техническое: ссылка не может пережить то, на что указывает.
fn main() {
let mut s = String::from("привет");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // сколько угодно читателей — можно
let w = &mut s;
w.push_str(", мир");
println!("{}", w); // один писатель — можно
}
Вывод:
привет привет привет, мир
Вопрос: «Почему компилятор запрещает две изменяемые ссылки одновременно?»
Что на самом деле проверяет интервьюер
Видишь ли ты за правилом инженерный смысл, а не прихоть языка. Причин три, и хорошо назвать все.
- Гонки данных. Два пути записи в одно значение — это ровно определение data race, если рядом появятся потоки. Rust ловит его на однопоточном коде, поэтому многопоточный получается безопасным «бесплатно».
- Инвалидация.
Vecприpushможет перевыделить буфер. Все ссылки на элементы после этого указывали бы в освобождённую память. - Оптимизации. Гарантия отсутствия aliasing позволяет компилятору держать значение в регистре и не перечитывать его после каждого вызова — то, ради чего в C приходится вручную писать
restrict.
fn main() {
let mut s = String::from("привет");
let r1 = &mut s;
let r2 = &mut s;
println!("{} {}", r1, r2);
}
Ошибка компиляции:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:4:14
|
3 | let r1 = &mut s;
| ------ first mutable borrow occurs here
4 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | println!("{} {}", r1, r2);
| -- first borrow later used here
Смешивать &T и &mut T тоже нельзя
Самый жизненный пример — изменение вектора прямо во время обхода. В Java это ConcurrentModificationException в рантайме, в C++ — молча испорченная память, в Rust — ошибка сборки:
fn main() {
let mut v = vec![1, 2, 3];
for &item in &v {
if item == 2 {
v.push(10);
}
}
}
Ошибка компиляции:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:5:13
|
3 | for &item in &v {
| --
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | if item == 2 {
5 | v.push(10);
| ^^^^^^^^^^ mutable borrow occurs here
Цикл for держит общее заимствование &v до последней итерации, а push требует изменяемого. Стандартный обход — собрать изменения в отдельный Vec и применить после цикла, либо использовать индексы и v.len() вместо итератора.
Вопрос: «Что такое NLL и почему этот код внезапно компилируется?»
fn main() {
let mut s = String::from("привет");
let r1 = &s;
println!("{}", r1); // последнее использование r1
let r2 = &mut s; // до NLL здесь была ошибка
r2.push_str(", мир");
println!("{}", r2);
}
Вывод:
привет привет, мир
Что на самом деле проверяет интервьюер
Знаешь ли ты, что non-lexical lifetimes (NLL, включены начиная с edition 2018) изменили не правила, а способ измерять «одновременно». Раньше заимствование жило до конца лексического блока — до закрывающей фигурной скобки. Теперь оно живёт до последней точки, где ссылка реально используется. В примере выше r1 перестаёт существовать сразу после println!, поэтому &mut s ниже уже никому не мешает. Правило «одна &mut ИЛИ много &T» не изменилось — сузился интервал, на котором оно проверяется.
| Код ошибки | Ситуация |
| E0499 | две изменяемые ссылки на одно значение одновременно |
| E0502 | изменяемая и общая ссылка пересеклись во времени |
| E0505 | попытка переместить значение, пока оно заимствовано |
| E0506 | присваивание в значение, которое сейчас заимствовано |
| E0597 | ссылка живёт дольше, чем то, на что она указывает |
Типичные ошибки кандидатов
- Говорить «нельзя две ссылки», забывая слово «изменяемые». Общих ссылок может быть сколько угодно.
- Считать, что NLL ослабил правила заимствования. Он поменял только границы времени жизни ссылки; гарантии те же.
- Объяснять запрет двух
&mutтолько через многопоточность. В однопоточном коде он тоже нужен — из-за инвалидации итераторов и перевыделения буфера. - Путать изменяемость переменной и изменяемость заимствования:
&mut sтребует, чтобы самаsбыла объявлена какlet mut. - Не уметь предложить обходной путь для E0502. Ожидают вариант «накопить и применить после цикла» или «работать по индексам».
Как ответить кратко
Правила заимствования: в любой момент времени либо одна изменяемая ссылка
&mut T, либо любое число общих&T, и ссылка не может пережить своё значение. Две&mutзапрещены, потому что это гонка данных, инвалидация ссылок при перевыделении и потеря гарантии отсутствия aliasing для оптимизатора. NLL означает, что заимствование заканчивается на последнем использовании ссылки, а не на закрывающей скобке.
- Коды под рукой: E0499 — две
&mut, E0502 — смесь&и&mut. - Канонический пример:
v.push()внутриfor &item in &v. - Главная мысль: то, что в других языках падает в рантайме, здесь не проходит сборку.