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()обычно нужна ссылка.