Ошибка "cannot move out of borrowed content" в Rust: причины и исправление
Кратко: почему возникает ошибка
Ошибка "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 использует три правила владения:
- У каждого значения есть ровно один владелец.
- Когда владелец выходит из области видимости, значение удаляется.
- Вы можете либо иметь одну изменяемую ссылку, либо любое количество неизменяемых ссылок.
Когда вы берёте неизменяемую ссылку &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).
Ответов пока нет.
Войдите чтобы оставить ответ