Ты уже работал со ссылками. Они кажутся пассивным способом взглянуть на значение, не трогая его. Взял, прочитал, пошёл дальше. Ничего не сдвинулось. В большинстве языков это вся история.
Borrow checker знает, что происходит кое-что ещё.
Читальный зал
Представь читальный зал с единственной справочной книгой на столе. Несколько человек могут сидеть и читать одновременно - это нормально. Читатели не мешают друг другу. Книга лежит на месте.
Но если библиотекарю нужно забрать книгу для правки - добавить главу, переставить страницы - все читатели должны сначала уйти из-за стола. Не ради формальности. А потому что библиотекарь может заменить книгу целиком на новое издание. И тот, кто ещё держит палец на странице, обнаружит, что рука тычет в пустоту.
В читальном зале одно правило: читатели и библиотекарь не должны работать одновременно.
Безобидная ссылка
Ссылка кажется безопасной. Ты пишешь &tasks[0] - и получаешь доступ к первому элементу. Ничего
не переходит из рук в руки. Список по-прежнему твой. Несколько ссылок могут существовать одновременно
- разные части программы держат
&tasksпараллельно, и ничего не ломается. В этом и смысл ссылок.
Модель ломается, когда ты изменяешь значение, пока один из этих доступов ещё открыт. Любой
владеющий тип, управляющий памятью в 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.