Avatar ·

Как исправить ошибку "use of moved value" в Rust?

Привет! Я начал изучать Rust и постоянно натыкаюсь на ошибку "use of moved value". Вот простой пример, который её вызывает: ```rust fn main() { let s = String::from("привет"); let t = s; println!("{}", s); // ошибка: use of moved value: `s` } ``` Я понимаю, что Rust строго следит за владением, но не могу разобраться, как правильно передавать данные между переменными и функциями. Например, я хочу написать функцию, которая принимает строку, что-то с ней делает, а потом я хочу использовать эту строку снова в main: ```rust fn process(s: String) -> String { // какая-то обработка s } fn main() { let my_string = String::from("тест"); let result = process(my_string); println!("{}", my_string); // снова ошибка! } ``` Почему это происходит? В C++ я бы просто передал по ссылке или скопировал, но здесь это не работает. Как правильно организовать код, чтобы избежать этой ошибки? Объясните, пожалуйста, на примерах, что такое владение, заимствование и как с ними работать. <<>>

Понимание владения в Rust

Ошибка "use of moved value" — одна из самых частых у новичков в Rust. Она связана с фундаментальной концепцией языка — владением (ownership). В Rust у каждого значения есть ровно один владелец. Когда вы присваиваете значение другой переменной или передаёте его в функцию, владение перемещается (move), и исходная переменная становится недействительной.

Почему это происходит?

В вашем первом примере:

let s = String::from("привет");
let t = s; // владение перемещается из s в t
println!("{}", s); // ❌ s больше не владеет данными

Тип String не реализует трейт Copy (в отличие от простых типов вроде i32 или bool). Поэтому присваивание let t = s — это перемещение, а не копирование. После перемещения s становится недействительной.

Три способа решения

1. Клонирование (если копирование допустимо)

Если вам нужно оставить исходные данные нетронутыми, можно явно клонировать значение:

let s = String::from("привет");
let t = s.clone(); // создаём глубокую копию
println!("{}", s); // ✅ работает
println!("{}", t); // ✅ тоже работает

Но будьте осторожны: clone() может быть дорогим для больших структур данных.

2. Заимствование (передача по ссылке)

Самый эффективный способ — передать ссылку на данные, а не сами данные. Ссылка не перемещает владение:

fn process(s: &String) -> &String {
    // используем s, но не забираем владение
    s
}

fn main() {
    let my_string = String::from("тест");
    let result = process(&my_string); // передаём ссылку
    println!("{}", my_string); // ✅ my_string всё ещё владеет данными
}

Обратите внимание: функция принимает &String (ссылку), а не String. Вызов process(&my_string) создаёт ссылку, не перемещая владение.

3. Возврат владения (если функция должна модифицировать)

Если функция должна изменить строку и вернуть её обратно, можно вернуть владение:

fn process(mut s: String) -> String {
    s.push_str(" обработан");
    s // возвращаем владение обратно
}

fn main() {
    let my_string = String::from("тест");
    let my_string = process(my_string); // перемещаем, но сразу получаем обратно
    println!("{}", my_string); // ✅ теперь my_string снова владеет
}

Правило большого пальца

  • Если функция только читает данные — используйте &T (неизменяемую ссылку).
  • Если функция изменяет данные — используйте &mut T (изменяемую ссылку).
  • Если функция становится владельцем данных (например, сохраняет их в коллекции) — принимайте T по значению.

Пример с изменяемой ссылкой

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut my_string = String::from("привет");
    add_exclamation(&mut my_string);
    println!("{}", my_string); // "привет!"
}

Изменяемая ссылка &mut T позволяет функции модифицировать данные, но не забирает владение.

Освоив эти три подхода, вы перестанете бояться ошибки "use of moved value". Помните: Rust защищает вас от двойного освобождения памяти и гонок данных — это плата за безопасность, которая окупается надёжностью кода.

<<>>

Альтернативный подход: использование трейта Borrow

Если вы работаете с обобщённым кодом и не хотите жёстко привязываться к конкретному типу, можно использовать трейт Borrow из стандартной библиотеки. Это позволяет функциям принимать как владельца, так и ссылку:

use std::borrow::Borrow;

fn process<T: Borrow<str>>(data: T) -> String {
    let s: &str = data.borrow();
    format!("Обработано: {}", s)
}

fn main() {
    let owned = String::from("тест");
    let result = process(&owned); // можно передать ссылку
    println!("{}", owned); // ✅ владение не перемещено
    
    let borrowed = "статическая строка";
    let result2 = process(borrowed); // можно передать &str
}

Этот подход особенно полезен в библиотеках, где вы хотите дать пользователю гибкость: передавать как владеющие строки String, так и срезы &str. Функция borrow() возвращает ссылку на внутренние данные, не перемещая владение. Это элегантный способ избежать "use of moved value" в обобщённом коде, сохраняя при этом производительность.