Move-семантика и трейт Copy

Присваивание в Rust по умолчанию не копирует и не ссылается — оно передаёт владение.

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

Вопрос: «Что произойдёт после let s2 = s1; если s1 — это String?»

Любимый вопрос-ловушка: он занимает десять секунд и сразу показывает, писал ли кандидат на Rust хоть что-то сложнее hello world.

Что на самом деле проверяет интервьюер

Понимаешь ли ты, что у String есть буфер в куче, и что поверхностная копия трёх слов заголовка привела бы к двум владельцам одного буфера. При выходе из области видимости оба вызвали бы освобождение — классический double free. Rust решает это не глубоким копированием (дорого и неявно) и не подсчётом ссылок (рантайм), а перемещением: s1 объявляется невалидной.

fn main() {
    let s1 = String::from("привет");
    let s2 = s1;

    println!("{}", s1);
}

Ошибка компиляции:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
2 |     let s1 = String::from("привет");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

Правильный устный ответ: «владение переедет в s2, s1 станет недействительной, обращение к ней — ошибка компиляции E0382». Физически при этом ничего не происходит: байты в куче не трогаются, копируются только три слова заголовка, а исходная переменная просто вычёркивается компилятором.

Вопрос: «Почему для i32 такой код работает, а для String — нет?»

fn main() {
    let x = 5;
    let y = x;
    println!("{} {}", x, y);
}

Вывод:

5 5

Что на самом деле проверяет интервьюер

Знаешь ли ты про трейт Copy. Если тип реализует Copy, присваивание делает побитовую копию, а исходная переменная остаётся полностью рабочей — никакого move не происходит. Реализовать Copy может только тип, который целиком лежит на стеке и не владеет никакими внешними ресурсами.

ТипCopy?Почему
i32, u8, f64, bool, charдафиксированный размер, побитовая копия безопасна
&T (общая ссылка)дакопия ссылки не создаёт второго владельца данных
&mut Tнетиначе появились бы две изменяемые ссылки сразу
(i32, bool), [u8; 4]дакортежи и массивы из Copy-типов
String, Vec<T>, Box<T>нетвладеют буфером в куче и реализуют Drop

Отдельное правило, которое стоит назвать вслух: тип, реализующий Drop, не может реализовать Copy. Иначе один и тот же ресурс освобождался бы столько раз, сколько копий успели сделать.

Вопрос: «Чем clone отличается от Copy?»

Что на самом деле проверяет интервьюер

Различаешь ли ты неявное и явное, дешёвое и дорогое. Copy — маркерный трейт без методов: копирование происходит само, молча, и всегда стоит ровно столько, сколько занимает значение на стеке. Clone — обычный трейт с методом clone(), который надо вызвать руками и который может делать сколь угодно дорогую работу: для String это выделение нового буфера в куче и копирование всех байтов.

fn main() {
    let s1 = String::from("привет");
    let s2 = s1.clone();          // новый буфер в куче

    println!("{} / {}", s1, s2);
    println!("{}", s1.as_ptr() == s2.as_ptr());
}

Вывод:

привет / привет
false

Указатели разные — это два независимых буфера, у каждого свой владелец. Обрати внимание на объявление в стандартной библиотеке: pub trait Copy: Clone. Copy требует Clone, потому что Copy — это частный случай клонирования, при котором clone() сводится к побитовой копии. Отсюда и привычная пара в коде: #[derive(Clone, Copy)].

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

Передача аргумента по значению — тоже move, и это второй по частоте способ «потерять» переменную:

fn length(s: String) -> usize {
    s.len()
}

fn length_keep(s: String) -> (String, usize) {
    let n = s.len();
    (s, n)                       // возвращаем владение вызывающему
}

fn main() {
    let a = String::from("привет");
    println!("{}", length(a));   // a больше недоступна

    let b = String::from("привет");
    let (b, n) = length_keep(b); // владение вернулось
    println!("{} {}", b, n);
}

Вывод:

12
привет 12

Возвращать владение кортежем — рабочий, но неудобный приём. Правильное решение — заимствование (fn length(s: &String) -> usize или лучше &str), о нём следующий урок. На собеседовании полезно самому сказать: «здесь на практике я бы взял ссылку».

Типичные ошибки кандидатов

  • Говорить «s1 удалилась» или «стала null». Она не удалена и не обнулена — компилятор просто запрещает ей пользоваться, значение живёт в s2.
  • Считать move дорогой операцией. Перемещение — это копия нескольких слов заголовка; дорогое только clone().
  • Ставить .clone() везде, где ругается компилятор. Это работает, но это признак того, что кандидат не умеет в заимствование.
  • Путать Copy и Clone местами: Copy неявный и дешёвый, Clone явный и любой.
  • Утверждать, что Copy можно навесить на любой тип через derive. Нельзя: если внутри есть String, Vec или свой Drop, компилятор откажет.

Как ответить кратко

Присваивание в Rust перемещает владение: после let s2 = s1; строка принадлежит s2, а s1 недействительна — обращение к ней даёт E0382. Для типов, реализующих Copy (числа, bool, char, общие ссылки, кортежи из них), вместо move делается побитовая копия, и исходная переменная остаётся рабочей. Clone — это явное и потенциально дорогое глубокое копирование через .clone().

  • Почему так: две поверхностные копии String означали бы двух владельцев одного буфера и double free.
  • Граница: Copy может быть только у типов без Drop и без данных в куче.
  • Практика: вместо clone() обычно нужна ссылка.
Проверьте себя
1. Что происходит с s1 после let s2 = s1;, если s1 имеет тип String?
As1 становится пустой строкой
Bs1 и s2 указывают на один буфер и обе рабочие
CВладение переходит к s2, а s1 становится недействительной
DСоздаётся глубокая копия, обе переменные независимы
2. Какой из типов НЕ реализует трейт Copy?
Achar
BString
C(i32, bool)
D&i32
3. Чем clone отличается от Copy?
AНичем, это два названия одной операции
Bclone копирует только стек, а Copy ещё и кучу
CCopy вызывается явно, а clone подставляется компилятором
DCopy — неявная побитовая копия, clone — явный и потенциально дорогой вызов метода