Почему этот 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 шт):

Автор решения: Damir Hakimof

В первом случае у вас неявно "удаляется" неизменяемая ссылка, вы ее не используете: все ОК.

Во втором ссылка неизменяемая также "удаляется", но вы хотите ее использовать: Ошибка.

Правило: либо один писатель, либо много читателей обязано работать. Неизменяемые ссылки в ваших примерах просто неявно "удаляются" после создания мутабельной.

→ Ссылка
Автор решения: extrn

Компилятор попытается минимизировать область существования ссылки, разбив код примерно на такие блоки - достаточные для того, чтобы все ссылки были доступны там, где они используются. В начале каждого блока будет создана ссылка, а к концу блока она перестанет существовать.

{
    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);  // здесь все ок, не будь этого блока посередине
}

И получится, что изменяемая ссылка создается при живой неизменяемой, что недопустимо.

→ Ссылка
Автор решения: Pak Uula

Начиная с 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

→ Ссылка