← Свитки rust

Самый простой способ понять владение в Rust

Rust забрал моё значение. Что происходит - и как перестать с этим бороться

Самый простой способ понять владение в Rust

Вы уже писали функции. Вы передавали значения. В Python, в JavaScript, в Go - всегда все одинаково: передал, a оно попрежнему у тебя. Такова модель, к которой вы привыкли.

И вот Rust говорит, что значение исчезло.

Без предупреждения. Без исключения. Просто ошибка компилятора на строке, которая выглядит совершенно нормально - и слово, которое вы раньше не видели в таком контексте: moved.

Вы его не удаляли. Не освобождали. Просто передали в функцию. А Rust говорит, что его больше нет.

Почему Rust устроен именно так

Любая программа использует память. И в какой-то момент её нужно освободить.

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

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

Владение(Ownership) - это механизм, который это обеспечивает.

Ключ

Представьте физический ключ. Один экземпляр, один владелец. Можно одолжить на час, но пока ключ в чужом кармане - в комнату не войти. Можно передать насовсем - но тогда и дверь закрыта для вас. Компилятор знает, где находится каждый ключ. Он никогда не гадает. Он никогда не забывает.

Неверная модель

В большинстве языков программирования передать значение в функцию - всё равно что показать кому-то документ: вы по-прежнему держите его в руках. Он посмотрел, использовал - ничего не изменилось. Эта модель разумна. Так работает Python. Так работает JavaScript. Rust работает иначе.

Проблема

Вот код, который выглдит логично в привычном стиле:

fn main() {
    let title = String::from("Buy coffee beans"); // ключ
    print_task(title);                            // ключ передан на обработку
    println!("Reminder: {}", title);              // он же все ещё у меня, правда?
}

fn print_task(t: String) {
    println!("Task: {}", t);                     // значение используется и уничтожается
}

Компилятор не согласен:

error[E0382]: borrow of moved value: `title`
 --> src/main.rs:4:28
  |
2 |     let title = String::from("Buy coffee beans");
  |         ----- move occurs because `title` has type `String`,
  |               which does not implement the `Copy` trait
3 |     print_task(title);
  |                ----- value moved here
4 |     println!("Reminder: {}", title);
  |                              ^^^^^ value borrowed here after move

Он не говорит “вы забыли скопировать”. Он говорит: moved here. И это намеренный выбор.

Один ключ, один владелец

Когда вы передали title в print_task, вы отдали ключ. Не копию - сам ключ. Функция теперь владеет этой строкой. Она за неё отвечает. Когда print_task завершится, значение уничтожается: убрано, исчезло. К моменту, когда выполнение доходит до println!, комната уже заперта и пуста.

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

Некоторые значения ведут себя иначе.

let y = x с целым числом работает нормально - числа маленькие, фиксированного размера, копировать их дёшево. Rust копирует их автоматически.

Строки - нет. String может быть большим и может изменять свой размер. Копирование обходится дороже, поэтому Rust не делает это за вашей спиной.

Когда владелец уходит

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

fn main() {
    {
        let title = String::from("Buy coffee beans");
        // title действителен здесь
    }
    // title исчез. комната заперта. ключа больше нет.
}

Это не ограничение. Это гарантия. Вы всегда знаете точно, когда значение исчезает. Никаких сюрпризов. Никакой ручной уборки.

Мастер

Теперь рассмотрим функцию, которая берёт значение в своё владение и возвращает что-то обратно:

fn main() {
    let title = String::from("Buy coffee beans"); // ключ
    let title = make_urgent(title);     // ключ передан мастеру
    println!("Task: {}", title);        // такая же попытка но всё работает: другой ключ
}

fn make_urgent(t: String) -> String {
    t + " (!)"                          // старый ключ переплавлен, возвращён новый
}

make_urgent берёт ключ, переделывает его и возвращает новый. Исходная строка - сырьё, израсходованное в процессе. Новое присваивание (let title = ...) кладет результат на тоже место с новой биркой. Oчередной вызов работает. Там уже другой ключ.

Когда нужны два ключа

Но иногда двум владельцам действительно нужны независимые копии. Для этого в Rust есть .clone():

fn main() {
    let title = String::from("Buy coffee beans");
    let backup = title.clone();          // второй ключ, полная копия

    print_task(title);
    println!("Backup: {}", backup);      // backup по-прежнему актуален
}

Clone явный по замыслу. Когда вы его видите, вы знаете: создаётся полная копия, и это не бесплатно. Rust никогда не копирует сложные значения автоматически. Не попросили - не получите.

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

И теперь когда модель встала на своё место, кое-что меняется. Сообщения об ошибках те же - но вы уже другой.

“Value moved here” - это больше не компилятор, который вам мешает. Это компилятор, который показывает, где именно ключ сменил владельца. “Borrow of moved value” - не жалоба. Это карта.

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

Три правила

Всё вышесказанное вытекает из трёх правил. Вы уже видели их все - вот они, с ключом в голове:

Вот что такое владение. Не ограничение а контракт. Который компилятор проверяет вместо вас.


Ещё кое-что стоит знать: если функции нужно только прочитать значение, а не владеть им, Rust позволяет одолжить ключ временно. Ссылка (&String) - это ключ, переданный на время вызова и автоматически возвращённый, когда функция завершается. Комната остаётся вашей. Владение - это постоянная передача. Одолжить - не то же самое. Borrow checker - правило, которое регулирует одалживание. Но это уже новая проблема, и для неё нужна новая история.