Avatar ·

Ошибка "cannot move out of borrowed content" в Rust: причины и исправление

Привет! Я начал изучать Rust и столкнулся с ошибкой, которую не могу понять. Код: ```rust fn main() { let s = String::from("hello"); let r = &s; let s2 = *r; // Ошибка: cannot move out of borrowed content println!("{}", s2); } ``` Я думал, что `*r` разыменовывает ссылку и даёт мне владение строкой `s`. Но компилятор говорит: "cannot move out of `*r` which is behind a shared reference". Почему так? Ведь я просто хочу скопировать значение. То же самое происходит с векторами: ```rust fn get_first(v: &Vec) -> i32 { v[0] // Ошибка: cannot move out of borrowed content } ``` Я понимаю, что Rust строго следит за владением, но не понимаю, почему нельзя просто "взять" значение по ссылке. В других языках (C++, Python) это работает. Как правильно получить значение из ссылки? И почему это ограничение существует? Объясните, пожалуйста, как исправить эти примеры и в чём глубинная причина ошибки. Заранее спасибо! <<>>

Кратко: почему возникает ошибка

Ошибка "cannot move out of borrowed content" возникает, когда вы пытаетесь переместить (move) значение из-за ссылки. Rust запрещает это, потому что перемещение оставляет исходное место недействительным — а ссылка всё ещё может быть использована, что приведёт к висячей ссылке или неопределённому поведению.

Разбор первого примера

В коде:

let s = String::from("hello");
let r = &s;
let s2 = *r; // Ошибка

Вы создаёте неизменяемую ссылку r на s. Когда вы пишете *r, вы разыменовываете ссылку, но затем пытаетесь переместить значение String из s в s2. Это невозможно, потому что:

  • r — это &String (shared reference), а не &mut String.
  • Перемещение String сделало бы s недействительным, но r всё ещё указывает на s.
  • Rust не может гарантировать, что r больше не будет использоваться.

Правильные решения для первого примера

Вариант 1: Клонирование

Если вам действительно нужна копия данных, используйте clone():

let s = String::from("hello");
let r = &s;
let s2 = r.clone(); // Создаёт глубокую копию
println!("{}", s2); // hello
println!("{}", s);  // s всё ещё доступна

Вариант 2: Заимствование вместо перемещения

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

let s = String::from("hello");
let r = &s;
println!("{}", r); // Работает, потому что println! заимствует

Вариант 3: Взятие владения

Если вы хотите передать владение, уберите ссылку:

let s = String::from("hello");
let s2 = s; // Перемещение напрямую
// println!("{}", s); // Ошибка: s больше не владеет данными

Разбор второго примера (вектор)

fn get_first(v: &Vec<i32>) -> i32 {
    v[0] // Ошибка
}

Здесь i32 — это тип, реализующий Copy (копируется бит в бит). Но v[0] всё равно выдаёт ошибку? Нет, на самом деле этот код работает! Потому что i32 реализует трейт Copy, и индексная операция возвращает копию значения.

Ошибка возникнет, если тип не реализует Copy, например, для Vec<String>:

fn get_first_string(v: &Vec<String>) -> String {
    v[0].clone() // Нужно клонировать
}

Глубинная причина: правила владения

Rust использует три правила владения:

  1. У каждого значения есть ровно один владелец.
  2. Когда владелец выходит из области видимости, значение удаляется.
  3. Вы можете либо иметь одну изменяемую ссылку, либо любое количество неизменяемых ссылок.

Когда вы берёте неизменяемую ссылку &T, вы обещаете не изменять и не перемещать данные, пока ссылка существует. Перемещение из-за ссылки нарушает это обещание — исходный владелец теряет данные, а ссылка становится висячей.

Как это исправить в общем случае

  • Для типов с Copy (i32, f64, bool, char, кортежи из Copy-типов) — просто разыменовывайте, копирование произойдёт автоматически.
  • Для типов без Copy (String, Vec, Box) — используйте clone() или измените дизайн: передавайте владение, а не ссылку.
  • Если нужно изменить данные — используйте &mut T.

Помните: Rust защищает вас от ошибок времени выполнения. В C++ вы бы получили висячий указатель и неопределённое поведение. В Rust вы получаете ошибку компиляции — это хорошо!

<<>>

Альтернативный подход: изменение сигнатуры функции

Вместо того чтобы бороться с заимствованием, можно перепроектировать код. Если функция должна получить владение значением, принимайте его по значению, а не по ссылке:

fn consume_string(s: String) {
    println!("{}", s);
} // s удаляется здесь

let s = String::from("hello");
consume_string(s); // Владение передано
// println!("{}", s); // Ошибка: s больше не существует

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

fn borrow_string(s: &String) {
    println!("{}", s);
}

let s = String::from("hello");
borrow_string(&s);
println!("{}", s); // Всё хорошо

Использование трейта AsRef

Для универсального кода, который работает и с владением, и с ссылками, используйте трейты:

fn print_it(s: impl AsRef<str>) {
    println!("{}", s.as_ref());
}

let owned = String::from("hello");
let borrowed: &str = "world";

print_it(&owned); // Работает
print_it(borrowed); // Работает
print_it(owned); // Работает, владение передано

Этот подход особенно полезен в публичных API, где вы хотите дать пользователям гибкость.

Когда всё-таки нужно перемещение из ссылки

Иногда это законно — например, для Option::take() или std::mem::replace(). Но эти методы требуют &mut T, а не &T. Запомните: из неизменяемой ссылки можно только читать или копировать (если тип реализует Copy).