Почему Rust ругается на borrow и как это исправить? Полный разбор

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

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

Начать курс

Введение: Момент, когда Rust вас «не понимает»

Вы пишете на Rust. Всё идёт хорошо. Компилятор молчит. Вы чувствуете себя повелителем памяти. А потом — бац! — появляется она:

error[E0507]: cannot move out of borrowed content
  --> src/main.rs:10:9
   |
10 |         let value = self.data;
   |                     ^^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait

Или её близкая родственница:

error[E0507]: cannot move out of `*self` which is behind a shared reference

Если вы новичок, это похоже на магическое проклятие. Если опытный — вы знаете, что это просто очередной урок от Borrow Checker (проверщика заимствований).

В этой статье мы разберём, что означают эти ошибки, почему Rust так строг, и — самое главное — как их исправить с помощью живых примеров кода. Это не скучная теория, а практический гид по выживанию в мире владения и заимствования.

Коротко о главном: Владение, заимствование и «перемещение»

Прежде чем лезть в дебри, давайте вспомним три столпа Rust:

  • Владение (Ownership): У каждого значения в Rust ровно один владелец.
  • Заимствование (Borrowing): Вы можете «одолжить» значение по ссылке (&T — неизменяемая, &mut T — изменяемая), не забирая владение.
  • Перемещение (Move): Когда вы присваиваете значение новой переменной или передаёте в функцию без &, владение перемещается. Исходная переменная становится недействительной.

Ошибка cannot move out of borrowed content возникает именно тогда, когда вы пытаетесь переместить значение, на которое у вас есть только ссылка (&T). Rust запрещает это, потому что после перемещения ссылка станет «висячей» (dangling pointer). А Rust, как вы знаете, таких вещей не прощает.

Сценарий 1: Попытка переместить поле структуры через &self

Самый частый случай. У вас есть структура, и вы хотите вернуть одно из её полей из метода, который принимает &self.

Код с ошибкой

struct User {
    name: String,
    age: u32,
}

impl User {
    // Пытаемся отдать name "насовсем"
    fn give_name(&self) -> String {
        self.name // ОШИБКА!
    }
}

fn main() {
    let user = User { name: "Alice".to_string(), age: 30 };
    let name = user.give_name();
    println!("{}", name);
}

Компилятор скажет:

error[E0507]: cannot move out of `self.name` which is behind a shared reference

Почему? self — это &User. Мы не владеем self, мы лишь одолжили его. Попытка переместить self.name (тип String, который не реализует Copy) означает, что после вызова метода владелец (user) потеряет своё поле. Но ссылка &self всё ещё существует! Rust защищает нас от катастрофы.

Как исправить?

Есть три основных пути, в зависимости от вашей задачи.

Способ 1: Клонирование (самый простой, но не всегда эффективный)

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

fn give_name(&self) -> String {
    self.name.clone() // Работает! Создаём копию.
}

Минус: clone() для String делает полную копию данных в куче. На больших строках это дорого.

Способ 2: Возврат ссылки (изменяем сигнатуру)

Если вызывающему коду достаточно ссылки на поле:

fn give_name(&self) -> &String {
    &self.name // Возвращаем ссылку, перемещения нет
}

Теперь мы не перемещаем, а заимствуем. Всё честно. Вызывающий код получит &String, а не String.

Способ 3: Забираем владение (меняем &self на self)

Если метод должен «потребить» структуру, измените сигнатуру:

fn give_name(self) -> String {
    self.name // Теперь self — владелец, можно перемещать
}

После вызова этого метода структура user будет перемещена и больше не доступна. Используйте, если структура больше не нужна.

Сценарий 2: Работа с Option и Result

Ещё один популярный случай — попытка достать значение из Option или Result, который находится за ссылкой.

Код с ошибкой

struct Container {
    item: Option<String>,
}

impl Container {
    fn take_item(&self) -> String {
        self.item.unwrap() // ОШИБКА!
    }
}

Метод unwrap() пытается переместить String из Option. Но self.item — это &Option<String>. Мы не можем забрать значение.

Исправление: as_ref() и cloned()

Стандартная библиотека Rust предлагает элегантные решения:

fn take_item(&self) -> Option<&String> {
    self.item.as_ref() // Даёт Option<&String>
}

// Или если нужна копия:
fn take_item_cloned(&self) -> Option<String> {
    self.item.clone() // Клонируем весь Option
}

Метод as_ref() превращает &Option<T> в Option<&T> без перемещения. Это ключевой паттерн в Rust.

Сценарий 3: Итерация по коллекции, которой вы не владеете

Предположим, у вас есть &Vec<String>, и вы хотите переместить элементы в другой вектор:

fn collect_names(names: &Vec<String>) -> Vec<String> {
    names.into_iter().collect() // ОШИБКА!
}

Метод into_iter() на ссылке не даёт перемещать элементы. Правильный способ:

fn collect_names(names: &Vec<String>) -> Vec<String> {
    names.iter().cloned().collect() // Клонируем каждый элемент
}

Или, если вам нужны ссылки:

fn collect_names_ref(names: &Vec<String>) -> Vec<&String> {
    names.iter().collect()
}

Блоги

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