Почему этот Rust код компилируется, несмотря на создание изменяемой ссылки после неизменяемой?
Изучаю правила borrowing в Rust я запутался в том, когда borrow checker разрешает смешивать изменяемые и неизменяемые ссылки.
Согласно правилам заимствования, нельзя иметь изменяемую ссылку, пока существует неизменяемая ссылка. Однако этот код компилируется и работает без ошибок:
#[derive(Debug)]
struct Account {
id: u32,
balance: i32,
holder: String,
}
impl Account {
fn new(id: u32, holder: String) -> Self {
Account {
id,
holder,
balance: 0,
}
}
}
fn change_account(account: &mut Account) {
account.holder = String::from("new holder");
}
fn main() {
let mut account = Account::new(1, String::from("я"));
let account_ref = &account; // неизменяемая ссылка
change_account(&mut account); // изменяемая ссылка - почему это работает?
println!("{}", &account.holder);
}
А вот этот код НЕ компилируется:
fn main() {
let mut account = Account::new(1, String::from("я"));
let account_ref = &account; // неизменяемая ссылка
change_account(&mut account);
println!("{}", account_ref.holder); // ОШИБКА!
}
Версия rustc 1.88.0 (6b00bc388 2025-06-23)
Вопросы:
Почему первый код компилируется? В чем разница между этими двумя случаями? Как borrow checker определяет, когда можно создавать конфликтующие ссылки?
Заранее спасибо за объяснение!
Ответы (3 шт):
В первом случае у вас неявно "удаляется" неизменяемая ссылка, вы ее не используете: все ОК.
Во втором ссылка неизменяемая также "удаляется", но вы хотите ее использовать: Ошибка.
Правило: либо один писатель, либо много читателей обязано работать. Неизменяемые ссылки в ваших примерах просто неявно "удаляются" после создания мутабельной.
Компилятор попытается минимизировать область существования ссылки, разбив код примерно на такие блоки - достаточные для того, чтобы все ссылки были доступны там, где они используются. В начале каждого блока будет создана ссылка, а к концу блока она перестанет существовать.
{
let account_ref = &account; // неизменяемая ссылка
}
{
let temp_mut_ref = &mut account; // изменяемая ссылка. предыдущая уже не существует
change_account(temp_mut_ref);
}
{
let temp_ref = &account; // неизменяемая ссылка. обе предыдущие не существуют
{
let temp_holder_ref = &(temp_ref.holder); // скобки просто для ясности
println!("{}", temp_holder_ref);
}
}
Во втором случае, у него не получится разбить код так, чтобы блоки не пересекались, т.к. account_ref используется дальше по тексту
{
let account_ref = &account; // неизменяемая ссылка
{
let temp_mut_ref = &mut account; // ОШИБКА на самом деле здесь
change_account(temp_mut_ref);
}
println!("{}", account_ref.holder); // здесь все ок, не будь этого блока посередине
}
И получится, что изменяемая ссылка создается при живой неизменяемой, что недопустимо.
Начиная с rust2018 компилятор Rust использует NLL - non-lexical lifetime:
the lifetime of a reference lasts only for those portions of the function in which the reference may later be used
Первый пример
fn main() {
let mut account = Account::new(1, String::from("я"));
let account_ref = &account;
// account_ref далее не используется, поэтому дропается здесь
// к этому моменту account_ref уже дропнуто, поэтому
// можно создавать мутабельную ссылку
change_account(&mut account);
// &mut account далее не используется, поэтому дропается здесь
// к этому моменту на account уже нет ссылок, поэтому
// можно брать ссылку на account.holder
println!("{}", &account.holder);
}
В первом примере account_ref не используется в коде функции main, поэтому компилятор завершает её lifetime сразу после let account_ref = &account;. Поэтому правило исключительности мутабельных ссылок не нарушается - к моменту создания ссылки &mut account ссылку account_ref компилятор уже дропнул.
Опять же, мутабельная ссылка не используется после вызова change_account, и компилятор её дропает срезу после завершения вызова. Поэтому к моменту println! на account нет ссылок, и можно безопасно получить ссылку на поле account.holder
Второй пример.
fn main() {
let mut account = Account::new(1, String::from("я"));
let account_ref = &account;
// account_ref используется дальше, поэтому lifetime продолжается
// иммутабельная ссылка account_ref ещё жива, поэтому
// мутабельную ссылку создавать нельзя
change_account(&mut account); // cannot borrow `account` as mutable because it is also borrowed as immutable
println!("{}", account_ref.holder);
}
Во втором примере lifetime ссылки account_ref длится до завершения println! и пересекается с lifetime ссылки &mut account. Поэтому компилятор сообщает об ошибке - есть ссылки &'a и &'b mut с пересекающимися 'a и `'b'
Собственно, именно об этом пишет компилятор
error[E0502]: cannot borrow `account` as mutable because it is also borrowed as immutable
--> src/main.rs:32:20
|
31 | let account_ref = &account; // неизменяемая ссылка
| -------- immutable borrow occurs here
32 | change_account(&mut account);
| ^^^^^^^^^^^^ mutable borrow occurs here
33 | println!("{}", account_ref.holder); // ОШИБКА!
| ------------------ immutable borrow later used here