Перемещение и копирование

Присваивание в Rust перемещает владение (move), а простые типы — копируются (Copy). Разбираем, почему.

Перемещение (move) — при присваивании значения из кучи владение переходит к новой переменной, а старая становится недействительной.

Move: владение переходит

Вернёмся к аналогии с единственной книгой. Если Алиса передаёт книгу Бобу, у неё самой книги больше нет — иначе их стало бы две. В Rust присваивание значения из кучи работает так же: владение перемещается, а старая переменная становится недействительной.

fn main() {
    let s1 = String::from("привет");
    let s2 = s1;            // владение строкой ПЕРЕМЕЩАЕТСЯ из s1 в s2
    // println!("{s1}");    // ОШИБКА: value borrowed here after move
    println!("{s2}");       // s2 — новый владелец, всё в порядке
}

Вывод:

привет

Почему так строго? String хранит данные в куче, а сама переменная — это указатель на них. Если бы s1 и s2 остались оба действительны, оба указывали бы на одну память. Когда оба вышли бы из области видимости, Rust попытался бы освободить её дважды — классический баг «двойного освобождения». Move решает это: после перемещения старая переменная просто запрещена.

Copy: простые типы копируются

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

fn main() {
    let x = 5;
    let y = x;          // x КОПИРУЕТСЯ в y
    println!("{x} {y}");  // оба доступны — это ок
}

Вывод:

5 5

Типы с Copy: все целые и дробные числа, bool, char и кортежи, состоящие только из таких типов. String, Vec и всё, что владеет кучей, — не Copy, поэтому перемещаются.

Move при передаче в функцию

То же правило действует при вызове функции: передача значения по значению перемещает владение внутрь функции. После вызова исходная переменная недействительна.

fn print_and_drop(s: String) {
    println!("{s}");
} // s удаляется здесь

fn main() {
    let text = String::from("данные");
    print_and_drop(text);   // владение text уходит в функцию
    // println!("{text}");  // ОШИБКА: text перемещён
}

Вывод:

данные

Как вернуть владение

Функция может вернуть значение, передав владение обратно. Но постоянно перемещать значение туда-сюда неудобно. Для этого есть заимствование — следующий урок.

Клонирование, когда копия нужна

Если действительно нужна вторая независимая копия данных из кучи, её делают явно через .clone(). Это честно: вы видите, что происходит дорогое копирование.

fn main() {
    let s1 = String::from("привет");
    let s2 = s1.clone();    // глубокая копия: своя память в куче
    println!("{s1} и {s2}");  // оба владеют своими данными
}

Вывод:

привет и привет

Итог

  • Присваивание данных из кучи (String, Vec) перемещает владение; старая переменная запрещена.
  • Простые типы на стеке (числа, bool, char) реализуют Copy и копируются.
  • Передача в функцию тоже перемещает владение; .clone() делает явную глубокую копию.
Проверьте себя
1. Что происходит с s1 после let s2 = s1, если s1 имеет тип String?
As1 копируется и остаётся доступной
BВладение перемещается в s2, а s1 становится недействительной
CВозникает ошибка времени выполнения
Ds1 и s2 указывают на одну память и обе доступны
2. Почему числа можно использовать после присваивания другой переменной?
AОни хранятся в куче
BОни реализуют трейт Copy и копируются целиком
CRust делает для них исключение случайно
DОни неизменяемы
3. Как сделать независимую глубокую копию String?
AПросто присвоить переменной
BВызвать метод .clone()
CДобавить mut
DИспользовать &
Поддержать проект