← Свитки rust

У borrow checker одно правило - и это не то, о котором ты думаешь

Почему borrow checker отвергает код, который выглядит безопасно - и что он на самом деле проверяет

У borrow checker одно правило - и это не то, о котором ты думаешь

Ты уже работал со ссылками. Они кажутся пассивным способом взглянуть на значение, не трогая его. Взял, прочитал, пошёл дальше. Ничего не сдвинулось. В большинстве языков это вся история.

Borrow checker знает, что происходит кое-что ещё.

Читальный зал

Представь читальный зал с единственной справочной книгой на столе. Несколько человек могут сидеть и читать одновременно - это нормально. Читатели не мешают друг другу. Книга лежит на месте.

Но если библиотекарю нужно забрать книгу для правки - добавить главу, переставить страницы - все читатели должны сначала уйти из-за стола. Не ради формальности. А потому что библиотекарь может заменить книгу целиком на новое издание. И тот, кто ещё держит палец на странице, обнаружит, что рука тычет в пустоту.

В читальном зале одно правило: читатели и библиотекарь не должны работать одновременно.

Безобидная ссылка

Ссылка кажется безопасной. Ты пишешь &tasks[0] - и получаешь доступ к первому элементу. Ничего не переходит из рук в руки. Список по-прежнему твой. Несколько ссылок могут существовать одновременно

Модель ломается, когда ты изменяешь значение, пока один из этих доступов ещё открыт. Любой владеющий тип, управляющий памятью в heap - Vec, String, HashMap - может перераспределить внутренний буфер при росте: выделяется новый, больший, всё переносится туда, старый освобождается. Любая ссылка в старое место теперь указывает на освобождённую память. Библиотекарь не просто добавил главу - он перенёс всю книгу на другую полку и выбросил старый экземпляр.

Библиотекарь приходит

Вот столкновение в коде. Читатель уже за столом.

fn main() {
    let mut tasks: Vec<String> = vec![
        String::from("Buy coffee"),
        String::from("Write tests"),
    ];

    let first = &tasks[0];                       // читатель садится
    tasks.push(String::from("Deploy"));          // библиотекарь взял книгу для правок
    println!("First task: {}", first);           // палец тычет в пустоту
}
error[E0502]: cannot borrow `tasks` as mutable because it is also borrowed as immutable
 --> src/main.rs:8:5
  |
7 |     let first = &tasks[0];
  |                  ----- immutable borrow occurs here
8 |     tasks.push(String::from("Deploy"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
9 |     println!("First task: {}", first);
  |                                ----- immutable borrow later used here

Компилятор не запрещает push. Он запрещает push, в момент пока first ещё за столом. first появляется на строке 7 и живёт до строки 9 - push на строке 8 попадает в этот промежуток. Rust выводит это из структуры кода до запуска, без проверок во время выполнения.

Пустой стол

Решение - освободить стол до прихода библиотекаря. Если хранить first не нужно, читайте значение там, где оно нужно: заимствование откроется и закроется в тот же момент.

fn main() {
    let mut tasks: Vec<String> = vec![
        String::from("Buy coffee"),
        String::from("Write tests"),
    ];

    println!("First task: {}", tasks[0]);    // читатель пришёл, прочитал, ушёл
    tasks.push(String::from("Deploy"));      // стол пуст - библиотекарь может работать
}

Нет хранимой ссылки, ничто не держит стол занятым. К моменту выполнения push читальный зал свободен.

Теперь всё становится на свои места

Когда появляется ошибка borrow checker, вопрос меняется. Не “почему Rust отвергает мой код?” - а “кто ещё сидит за столом?” Найди заимствование, которое всё ещё живёт, пойми, почему оно доходит так далеко, и спроси себя - можно ли завершить его раньше. В сообщениях об ошибке называет строки. Читальный зал объясняет, почему они конфликтуют.


Два правила порождают все ошибки borrow checker, с которыми ты столкнёшься:

Первое правило - это определение гонки данных, сделанной невозможной на этапе компиляции. Второе - определение висячего указателя, исключённого по построению. Не обнаруженного во время выполнения - невозможного в Rust.


Стоит знать: у этого нет стоимости во время выполнения. Никакой блокировки при взятии ссылки. Никакого счётчика при её завершении. Компилятор находи проблему статически - из структуры твоего кода, до запуска. Периодическая перестройка кода, которую требует borrow checker, - это цена гарантии: целый класс ошибок не может существовать в Rust.