«Use of moved value» в Rust: почему это происходит и как с этим жить
Если вы только начали изучать Rust, то наверняка уже столкнулись с загадочным сообщением: error[E0382]: use of moved value. Компилятор ругается, вы чешете затылок, а код, который логически абсолютно верен, отказывается компилироваться. Знакомо? Добро пожаловать в мир владения и заимствования — ключевых концепций Rust, которые делают его безопасным, но иногда ставят в тупик даже опытных разработчиков.
Эта статья — не сухая документация, а живой разбор проблемы. Мы рассмотрим, почему возникает ошибка use of moved value, на реальных примерах кода, и, главное, что с ней делать. Вы узнаете, как думать как borrow checker и перестанете с ним бороться. Поехали!
Что такое «move» в Rust и почему компилятор такой строгий?
В Rust у каждого значения есть ровно один владелец (owner). Когда вы присваиваете значение другой переменной или передаёте его в функцию, владение перемещается (move). После перемещения исходная переменная становится недействительной — компилятор запрещает к ней обращаться. Это и есть корень ошибки use of moved value.
Посмотрите на простейший пример:
let s1 = String::from("привет");
let s2 = s1; // Владение перемещается от s1 к s2
println!("{}", s1); // ОШИБКА: use of moved value: `s1`
Почему так? String — это не простая копия в стеке. Она хранит данные в куче. Если бы Rust просто скопировал указатель, то при освобождении памяти обе переменные попытались бы её удалить — double free. Вместо этого Rust «перемещает» владение: теперь s2 отвечает за память, а s1 больше не существует с точки зрения компилятора. Это основная идея move semantics в Rust.
Типичные сценарии, приводящие к ошибке
Ошибка use of moved value чаще всего встречается в трёх ситуациях:
- Передача значения в функцию
- Присваивание значения другой переменной
- Использование значения в цикле или итераторе
Рассмотрим каждый случай подробно.
1. Передача в функцию без возврата
fn print_string(s: String) {
println!("{}", s);
}
fn main() {
let my_string = String::from("Rust");
print_string(my_string); // владение перемещено
println!("{}", my_string); // ошибка! use of moved value
}
После вызова print_string владение my_string ушло в функцию. Когда функция завершается, s удаляется, и my_string больше недействительна. Компилятор защищает вас от использования мёртвого объекта.
2. Присваивание без копирования
Мы уже видели этот пример с String. Но важно: не все типы ведут себя так. Простые типы вроде i32, bool, f64 реализуют типаж Copy. Они не перемещаются, а копируются автоматически.
let x = 5;
let y = x; // копия, не перемещение
println!("{}", x); // работает!
Типы, которые хранят данные в куче (String, Vec, Box), не реализуют Copy — только Clone. Поэтому они перемещаются.
3. Итерация по коллекции
Ещё один частый источник ошибок — циклы for:
let v = vec![1, 2, 3];
for x in v { // v перемещается в итератор
println!("{}", x);
}
println!("{:?}", v); // ошибка! use of moved value
Когда вы пишете for x in v, вы фактически вызываете into_iter(), который потребляет вектор. После цикла v больше не существует.
Как исправить: 4 проверенных способа
Теперь перейдём к самому важному — решениям. Выбор метода зависит от контекста и того, что вы хотите сделать с данными.
Способ 1: Заимствование (borrowing) — самый частый
Если вам нужно только прочитать данные, не забирая владение, используйте ссылки. Передавайте &T (неизменяемое заимствование) или &mut T (изменяемое).
fn print_string(s: &String) {
println!("{}", s);
}
fn main() {
let my_string = String::from("Rust");
print_string(&my_string); // передаём ссылку
println!("{}", my_string); // работает! владение осталось у main
}
То же самое с вектором:
let v = vec![1, 2, 3];
for x in &v { // заимствуем элементы
println!("{}", x);
}
println!("{:?}", v); // работает!
Заимствование — это ваш основной инструмент. Оно не копирует данные и не требует дополнительных затрат. Используйте его всегда, когда не нужно передавать владение.
Способ 2: Клонирование (clone) — когда нужно сохранить оба значения
Если вам нужно оставить исходное значение и передать копию, используйте метод .clone(). Он создаёт глубокую копию данных в куче.
let s1 = String::from("привет");
let s2 = s1.clone(); // явное копирование
println!("{}", s1); // работает
println!("{}", s2); // работает
Но будьте осторожны: клонирование — дорогая операция, особенно для больших строк или векторов. Не злоупотребляйте им, если можно обойтись заимствованием.
Способ 3: Возврат владения из функции
Если функция должна изменить значение и вернуть его обратно, можно вернуть владение через возвращаемое значение. Это называется «перемещение туда и обратно».
fn add_world(s: String) -> String {
let mut result = s;
result.push_str(", world!");
result // возвращаем владение
}
fn main() {
let my_string = String::from("Hello");
let my_string = add_world(my_string); // владение перемещается и возвращается
println!("{}", my_string); // работает: "Hello, world!"
}
Этот паттерн часто используется, но он может сделать код громоздким. В реальных проектах предпочитают изменяемые ссылки (&mut T) или кортежи.
Способ 4: Типаж Copy для простых типов
Если ваш тип состоит только из данных, которые помещаются в стек, и не требует управления памятью, вы можете реализовать для него Copy. Но это не решение для String или Vec