Заимствование и правило одной изменяемой ссылки
Второй обязательный блок: правила ссылок. Спрашивают, сколько ссылок можно держать одновременно, почему push при живой ссылке - ошибка, что изменилось с NLL. Здесь проверяют, понимаешь ли ты, какой класс багов закрывает borrow checker, а не просто помнишь ли формулировку.
Стержень: либо много неизменяемых ссылок, либо одна изменяемая, и проверка полностью статическая.
// Формулировки: «сколько &mut можно взять?», «почему нельзя менять вектор при живой ссылке?», «что такое NLL?»
Или читатели, или писатель
Правило XOR (исключающее или): в один момент времени либо сколько угодно &, либо ровно одна &mut. Нарушение ловится компилятором: две изменяемые дают E0499, изменяемая при живой неизменяемой - E0502.
Это не формальность. Тем же правилом закрываются изменение коллекции во время итерации, висячий указатель после перевыделения буфера и гонка данных между потоками. Причём работает оно и в однопоточном коде: половина типовых ошибок с указателями - вовсе не про многопоточность.
// Компилятор рассуждает про переменные, а не про фактические адреса: два &mut на разные элементы вектора по индексам он тоже не пропустит, хотя пересечения нет.
let mut v = vec![1, 2];
let r = &v[0];
v.push(3); // E0502
println!("{r}");- XOR-правило
- чтение многими или запись одним, но не одновременно
- E0499 / E0502
- две &mut / &mut при живой &
NLL: заимствование живёт до последнего использования
До 2018-й редакции заимствование тянулось до конца области видимости, и приходилось вручную резать код на блоки. Теперь работает NLL (negative log likelihood): ссылка мертва после последнего места, где она реально используется.
Поэтому такой код собирается: взяли &mut, поработали, потом взяли второй &mut. Первая ссылка к моменту второго заимствования уже не нужна, конфликта нет. Если же между ними стоит использование первой ссылки - компилятор справедливо ругается.
// Практический вывод: старые советы «оберни в блок, чтобы borrow закончился» чаще всего уже не нужны. Если ошибка осталась, значит ссылка действительно живёт дольше, чем кажется.
let mut v = vec![1];
let a = &mut v;
a.push(2);
let b = &mut v; // ок: a мертва
b.push(3);- NLL
- нелексические лайфтаймы: заимствование до последнего использования
- reborrow
- временная &mut на время вызова функции
Когда правило мешает: split_at_mut
Типовая задача - параллельно менять две половины массива. Взять &mut v[0] и &mut v[1] нельзя: индексация заимствует контейнер целиком, и компилятор видит два эксклюзивных заимствования одного вектора.
Стандартный выход - split_at_mut: он возвращает два непересекающихся среза, и каждый можно менять независимо. Внутри метод написан через unsafe, но снаружи он безопасен, потому что автор доказал непересечение. Это образцовый пример безопасной абстракции: сложность спрятана в одном месте, а пользователи получают проверяемый API.
// Для итерации с изменением элементов есть iter_mut() - он тоже выдаёт по одной &mut за раз и укладывается в правило.
let mut v = vec![1, 2, 3, 4];
let (a, b) = v.split_at_mut(2);
a[0] = 9;
b[0] = 8;- split_at_mut
- разделение среза на две независимые &mut части
- безопасная абстракция
- безопасное API поверх внутреннего unsafe
Как отвечать: «Зачем нужно правило одной изменяемой ссылки?»
Оно даёт эксклюзивность на запись, а из неё следует сразу несколько гарантий. Нельзя менять коллекцию, пока по ней идёт итерация или живёт ссылка на элемент, то есть буфер не переедет из-под указателя. Нельзя одновременно писать в одно место из двух мест кода - значит гонка данных невозможна по построению, и это работает даже до того, как речь заходит о потоках. Проверка полностью статическая: нарушение не доходит до рантайма, компилятор роняет сборку с E0499 или E0502.
Ты называешь не правило, а его следствия, и показываешь, что понимаешь связь между заимствованием и безопасностью потоков. Это ответ на уровень middle+.
На чём валятся
- −Отвечают, что правило нужно ради многопоточности. Оно закрывает и однопоточные ошибки с указателями.
- −Считают, что заимствование живёт до конца блока это поведение до NLL.
- −Пробуют взять &mut на два элемента вектора по индексам и не знают про split_at_mut.
- −Путают E0499 и E0502 и не могут объяснить, что именно конфликтует.
- −Думают, что передача &mut в функцию перемещает ссылку. Это reborrow, оригинал остаётся живым.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Почему это собирается, хотя изменяемых ссылок на вектор две?
let mut v = vec![1, 2]; let a = &mut v; a.push(3); let b = &mut v; b.push(4);A)Вторая ссылка перекрывает первую по правилам shadowingB)push не удерживает заимствование: оно кончается внутри методаC)Заимствование живёт до последнего использования, а не до конца блокаD)Компилятор видит, что обе ссылки указывают на один вектор, и склеивает ихпоказать ответ и разбор
+C)Заимствование живёт до последнего использования, а не до конца блока// разбор: Это NLL: область заимствования кончается на последнем месте, где ссылка реально используется. После a.push(3) ссылка a мертва, вектор снова свободен — вторая &mut законна. До 2018-й редакции borrow тянулся до конца области видимости, и такой код приходилось разбивать на блоки; отсюда старые советы, которые сегодня уже не нужны.
- Функция принимает r: &mut Vec<i32> и вызывается дважды подряд с одной и той же ссылкой. Почему это работает, ведь &mut не Copy?A)Компилятор делает reborrow: на время вызова уходит новая ссылкаB)&mut всё-таки Copy, если тип за ней реализует SizedC)Работает только потому, что вызовы идут в одном выраженииD)Вторая передача клонирует ссылку через CloneMut
показать ответ и разбор
+A)Компилятор делает reborrow: на время вызова уходит новая ссылка// разбор: На месте аргумента компилятор подставляет &mut *r — временное переподзаимствование, которое живёт лишь на время вызова. Оригинальная ссылка на этот срок заморожена, а после возврата снова доступна. Поэтому &mut удобно передавать по цепочке функций, не возвращая владение.
- Нужно параллельно менять первую и вторую половину вектора. Почему let a = &mut v[0]; let b = &mut v[1]; не проходит и что берут вместо этого?A)Мешает проверка границ; берут unsafe-доступ get_unchecked_mutB)Индексация заимствует весь вектор; берут split_at_mutC)Индексация возвращает значение, а не ссылку; берут get_mut дваждыD)Мешает Deref у Vec; берут срез &mut v[..] и обычную индексацию
показать ответ и разбор
+B)Индексация заимствует весь вектор; берут split_at_mut// разбор: IndexMut заимствует контейнер целиком, поэтому вторая индексация — это вторая &mut на тот же вектор (E0499). Компилятор не отслеживает непересечение индексов. Стандартный выход — split_at_mut: он отдаёт два непересекающихся среза, и это законно, потому что внутри проверено на unsafe и оформлено безопасным API.
- Что означает & перед типом в сигнатуре fn f(s: &String)?A)Функция получает копию значения и вправе её менятьB)Значение будет освобождено сразу после возврата из функцииC)Аргумент необязателен: можно передать пустую ссылкуD)Функция берёт значение взаймы, владелец остаётся снаружи
показать ответ и разбор
+D)Функция берёт значение взаймы, владелец остаётся снаружи// разбор: Ссылка — это доступ без владения: функция читает значение, а освобождать его будет по-прежнему тот, кто им владеет. Поэтому после вызова переменная у вызывающего жива, в отличие от передачи по значению. Менять через & нельзя — для этого пишут &mut, и такая ссылка в один момент времени существует только одна.
- Чем &mut T отличается от &T на практике?A)&mut отдаёт копию значения, а & — доступ к оригиналу без копированияB)&mut живёт до конца функции, & — до конца выраженияC)Через &mut можно менять значение, и такая ссылка в этот момент однаD)&mut работает только с типами в куче
показать ответ и разбор
+C)Через &mut можно менять значение, и такая ссылка в этот момент одна// разбор: Общая ссылка даёт чтение и может сосуществовать с другими такими же. Изменяемая даёт запись и потому эксклюзивна: пока она жива, других ссылок на это значение нет. Это правило XOR — либо много читателей, либо один писатель — и оно закрывает целый класс ошибок, от изменения коллекции во время обхода до гонок данных.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.