сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Владение и лайфтаймы

Заимствование и правило одной изменяемой ссылки

Заимствование и правило XOR

Второй обязательный блок: правила ссылок. Спрашивают, сколько ссылок можно держать одновременно, почему 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, остальные разбираются в тренажёре.

  1. #rs_borrowing1 / 5
    Почему это собирается, хотя изменяемых ссылок на вектор две?
    let mut v = vec![1, 2];
    let a = &mut v;
    a.push(3);
    let b = &mut v;
    b.push(4);
    A)Вторая ссылка перекрывает первую по правилам shadowing
    B)push не удерживает заимствование: оно кончается внутри метода
    C)Заимствование живёт до последнего использования, а не до конца блока
    D)Компилятор видит, что обе ссылки указывают на один вектор, и склеивает их
    показать ответ и разбор
    +C)Заимствование живёт до последнего использования, а не до конца блока

    // разбор: Это NLL: область заимствования кончается на последнем месте, где ссылка реально используется. После a.push(3) ссылка a мертва, вектор снова свободен — вторая &mut законна. До 2018-й редакции borrow тянулся до конца области видимости, и такой код приходилось разбивать на блоки; отсюда старые советы, которые сегодня уже не нужны.

  2. #rs_borrowing2 / 5
    Функция принимает r: &mut Vec<i32> и вызывается дважды подряд с одной и той же ссылкой. Почему это работает, ведь &mut не Copy?
    A)Компилятор делает reborrow: на время вызова уходит новая ссылка
    B)&mut всё-таки Copy, если тип за ней реализует Sized
    C)Работает только потому, что вызовы идут в одном выражении
    D)Вторая передача клонирует ссылку через CloneMut
    показать ответ и разбор
    +A)Компилятор делает reborrow: на время вызова уходит новая ссылка

    // разбор: На месте аргумента компилятор подставляет &mut *r — временное переподзаимствование, которое живёт лишь на время вызова. Оригинальная ссылка на этот срок заморожена, а после возврата снова доступна. Поэтому &mut удобно передавать по цепочке функций, не возвращая владение.

  3. #rs_borrowing3 / 5
    Нужно параллельно менять первую и вторую половину вектора. Почему let a = &mut v[0]; let b = &mut v[1]; не проходит и что берут вместо этого?
    A)Мешает проверка границ; берут unsafe-доступ get_unchecked_mut
    B)Индексация заимствует весь вектор; берут split_at_mut
    C)Индексация возвращает значение, а не ссылку; берут get_mut дважды
    D)Мешает Deref у Vec; берут срез &mut v[..] и обычную индексацию
    показать ответ и разбор
    +B)Индексация заимствует весь вектор; берут split_at_mut

    // разбор: IndexMut заимствует контейнер целиком, поэтому вторая индексация — это вторая &mut на тот же вектор (E0499). Компилятор не отслеживает непересечение индексов. Стандартный выход — split_at_mut: он отдаёт два непересекающихся среза, и это законно, потому что внутри проверено на unsafe и оформлено безопасным API.

  4. #rs_borrowing4 / 5
    Что означает & перед типом в сигнатуре fn f(s: &String)?
    A)Функция получает копию значения и вправе её менять
    B)Значение будет освобождено сразу после возврата из функции
    C)Аргумент необязателен: можно передать пустую ссылку
    D)Функция берёт значение взаймы, владелец остаётся снаружи
    показать ответ и разбор
    +D)Функция берёт значение взаймы, владелец остаётся снаружи

    // разбор: Ссылка — это доступ без владения: функция читает значение, а освобождать его будет по-прежнему тот, кто им владеет. Поэтому после вызова переменная у вызывающего жива, в отличие от передачи по значению. Менять через & нельзя — для этого пишут &mut, и такая ссылка в один момент времени существует только одна.

  5. #rs_borrowing5 / 5
    Чем &mut T отличается от &T на практике?
    A)&mut отдаёт копию значения, а & — доступ к оригиналу без копирования
    B)&mut живёт до конца функции, & — до конца выражения
    C)Через &mut можно менять значение, и такая ссылка в этот момент одна
    D)&mut работает только с типами в куче
    показать ответ и разбор
    +C)Через &mut можно менять значение, и такая ссылка в этот момент одна

    // разбор: Общая ссылка даёт чтение и может сосуществовать с другими такими же. Изменяемая даёт запись и потому эксклюзивна: пока она жива, других ссылок на это значение нет. Это правило XOR — либо много читателей, либо один писатель — и оно закрывает целый класс ошибок, от изменения коллекции во время обхода до гонок данных.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.