«Use of moved value» в Rust: как исправить и не сойти с ума

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«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

Блоги

Рекомендаций по книгам