Вы уже писали функции. Вы передавали значения. В 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 - правило, которое регулирует
одалживание. Но это уже новая проблема, и для неё нужна новая история.