Перейти к содержимому

Ошибка Rust E0507: cannot move out of a value behind a shared reference

E0507 значит, что ты пытался забрать значение (переместить его) из того, что ты только заимствовал. На заимствованное значение можно смотреть, но унести его нельзя, ведь тогда у настоящего владельца осталась бы дыра. Ошибка очень частая, особенно с Vec<String>, полями структур и Option. Обычно исправление — заимствовать вместо перемещения или вызвать .clone(). Сначала на этой странице показаны исправления, а потом объяснение. Другие ошибки из этой серии собраны на странице Ошибки компилятора Rust.

error[E0507]: cannot move out of `self.name` which is behind a shared reference
--> src/main.rs:7:9
|
7 | self.name // ❌ tries to give away a String that `self` only borrows
| ^^^^^^^^^ move occurs because `self.name` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
7 | self.name.clone() // ❌ tries to give away a String that `self` only borrows
| ++++++++

Компилятор пишет: нельзя переместить self.name, потому что он за общей (неизменяемой) ссылкой, а String не реализует Copy. Он предлагает клонировать значение, если цена по производительности приемлема. Вот программа, которая выдаёт эту ошибку:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name // ❌ tries to give away a String that `self` only borrows
}
}
fn main() {
let u = User { name: String::from("Ferris") };
println!("{}", u.name());
}

Комментарий говорит: код пытается отдать String, которую self только заимствует. У этого кода ошибки есть несколько формулировок. Все они значат «ты это только заимствовал, поэтому переместить нельзя»:

  • cannot move out of index of `Vec<String>` для let first = names[0];
  • cannot move out of `*r` which is behind a shared reference для let owned = *r;, где r: &String
  • cannot move out of `nick` as enum variant `Some` which is behind a shared reference для match *nick { Some(n) => n, ... }
  • cannot move out of `report`, a captured variable in an `Fn` closure
  • cannot move out of `self.log` which is behind a mutable reference (да, даже &mut не позволяет просто забрать значение)
  • cannot move out of an `Rc` для let s: String = *shared;

Посмотри на строку 7. Метод возвращает String — владеющее значение. Значит, self.name переместил бы строку из структуры и отдал вызывающему коду. Но у метода есть только &self, общее заимствование (только для чтения) пользователя. Он не владеет пользователем, поэтому не может отдать его имя.

Часть move occurs because ... does not implement the Copy trait тоже важна. Если бы поле было u32, Rust молча скопировал бы его, и ошибки не было бы. String владеет памятью в куче, поэтому её можно только переместить, но не скопировать.

Представь, что ты взял у друга книжную полку на вечер. Читать любую книгу на ней можно. Но унести книгу домой нельзя: полка не твоя, и друг вернётся к пустому месту.

У каждого значения ровно один владелец, и владелец убирает его, когда сам исчезает (см. урок Владение). Если бы можно было переместить self.name из заимствованного User, пользователь всё равно считал бы, что владеет этой строкой. Позже пользователь удаляется и освобождает строку второй раз — или кто-то читает поле, которого уже нет. Это «использование после освобождения» (use after free) или «двойное освобождение» (double free), две из худших ошибок в C и C++.

С Vec так же. names[0] по-прежнему принадлежит вектору. Если бы let first = names[0]; перемещал элемент наружу, в векторе на индексе 0 осталась бы дыра, и при удалении вектор попытался бы освободить эту ячейку ещё раз. Поэтому индексация даёт только доступ, но никогда не владение.

Правило в одну строку: через ссылку можно читать и (с &mut) менять, но забирать нельзя. Если тебе нужно владение, ты должен владеть контейнером или оставить что-то на месте значения.

Исправление 1: заимствуй вместо перемещения

Заголовок раздела «Исправление 1: заимствуй вместо перемещения»

Выбирай это, когда значение нужно только читать. Чаще всего это правильное исправление. Возвращай &str вместо String, пиши &v[0] вместо v[0] и заимствуй содержимое Option через as_deref() или as_ref():

struct User {
name: String,
nickname: Option<String>,
}
impl User {
fn name(&self) -> &str {
&self.name
}
fn display_name(&self) -> &str {
match self.nickname.as_deref() {
Some(nick) => nick,
None => &self.name,
}
}
}
fn describe(nick: &Option<String>) -> &str {
match nick {
Some(n) => n,
None => "аноним",
}
}
fn main() {
let users = vec![
User { name: String::from("Ferris"), nickname: Some(String::from("краб")) },
User { name: String::from("Corro"), nickname: None },
];
let first = &users[0];
println!("Первый пользователь: {}", first.name());
for user in &users {
println!("Отображается как: {}", user.display_name());
}
println!("Никнейм: {}", describe(&users[1].nickname));
}

Вывод:

Первый пользователь: Ferris
Отображается как: краб
Отображается как: Corro
Никнейм: аноним
▶ Попробуй сам

Посмотри на describe: она сопоставляет nick (ссылку), а не *nick. Тогда n — это &String, заимствование, и ничего не перемещается. Если написать match *nick { Some(n) => ... }, ты просишь владеющую String в n, а это E0507. Подсказка компилятора говорит о том же: consider removing the dereference here (попробуй убрать разыменование).

as_deref() превращает &Option<String> в Option<&str>; as_ref() даёт Option<&String>. Вызывай их перед .unwrap(), .map() или match, чтобы работать с заимствованием, а не с владеющим значением. То же для *r, где r: &String: обычно * вообще не нужен, просто используй r. Подробнее в уроках Option и Заимствование.

Выбирай это, когда тебе действительно нужна своя копия, например чтобы сохранить её где-то ещё, пока оригинал остаётся на месте. Компилятор предлагает это почти в каждом сообщении E0507:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name.clone()
}
}
fn main() {
let names = vec![String::from("Ferris"), String::from("Corro")];
let first = names[0].clone();
println!("{first} — первый, а в списке по-прежнему {} имени", names.len());
let u = User { name: String::from("Ferris") };
let mut badge = u.name();
badge.push_str(" (админ)");
println!("{badge} / {}", u.name);
}

Вывод:

Ferris — первый, а в списке по-прежнему 2 имени
Ferris (админ) / Ferris
▶ Попробуй сам

Клонировать имя или короткий список дёшево, и это совершенно нормально, особенно пока ты учишься. Проблемой это становится только для больших данных в горячем цикле. Если хватает заимствования (исправление 1), выбирай его.

Исправление 3: забери значение и оставь что-то взамен

Заголовок раздела «Исправление 3: забери значение и оставь что-то взамен»

Выбирай это, когда у тебя есть доступ &mut и ты хочешь по-настоящему переместить значение наружу, например отдать готовый лог или следующую задачу. Дыру оставить нельзя, но можно подложить замену:

  • std::mem::take(&mut x) забирает значение и оставляет значение по умолчанию ("" для String, пустой Vec для Vec).
  • option.take() забирает содержимое Option и оставляет None.
  • std::mem::replace(&mut x, new) кладёт new и возвращает тебе старое значение.
use std::mem;
#[derive(Debug)]
enum Status {
Queued,
Running,
}
struct Job {
log: String,
next_step: Option<String>,
status: Status,
}
impl Job {
fn take_log(&mut self) -> String {
mem::take(&mut self.log)
}
fn start_next(&mut self) -> Option<String> {
self.next_step.take()
}
fn set_status(&mut self, new: Status) -> Status {
mem::replace(&mut self.status, new)
}
}
fn main() {
let mut job = Job {
log: String::from("сборка прошла"),
next_step: Some(String::from("деплой")),
status: Status::Queued,
};
let old = job.set_status(Status::Running);
println!("Статус: {old:?} -> {:?}", job.status);
println!("Следующий шаг: {:?}", job.start_next());
println!("Следующий шаг ещё раз: {:?}", job.start_next());
println!("Лог: {:?}", job.take_log());
println!("Лог теперь: {:?}", job.log);
}

Вывод:

Статус: Queued -> Running
Следующий шаг: Some("деплой")
Следующий шаг ещё раз: None
Лог: "сборка прошла"
Лог теперь: ""
▶ Попробуй сам

В структуре никогда не остаётся дыры: после take_log лог — пустая строка, после start_next шаг — None. Это стандартное исправление для «cannot move out of self.x which is behind a mutable reference». mem::take требует, чтобы тип реализовал Default; для других типов используй mem::replace.

Исправление 4: владей коллекцией, а потом перемещай из неё

Заголовок раздела «Исправление 4: владей коллекцией, а потом перемещай из неё»

Выбирай это, когда коллекция или структура тебе больше не нужна и ты хочешь её содержимое. Если Vec принадлежит тебе, он может отдавать элементы: remove(i), swap_remove(i), pop() или into_iter() для всех сразу. Метод, который принимает self (а не &self), владеет структурой и может перемещать из неё поля:

struct User {
name: String,
}
impl User {
fn into_name(self) -> String {
self.name
}
}
fn main() {
let mut queue = vec![String::from("почта"), String::from("бэкап"), String::from("отчёт")];
let first = queue.remove(0);
println!("Делаю: {first}, осталось: {queue:?}");
let users = vec![User { name: String::from("Ferris") }, User { name: String::from("Corro") }];
let names: Vec<String> = users.into_iter().map(|u| u.into_name()).collect();
println!("{names:?}");
}

Вывод:

Делаю: почта, осталось: ["бэкап", "отчёт"]
["Ferris", "Corro"]
▶ Попробуй сам

into_iter() поглощает вектор, поэтому после этого users использовать нельзя (это была бы E0382). Приставка into_ в into_name — привычка именования в Rust: она говорит читателю «это расходует значение». remove(0) сдвигает все остальные элементы; если порядок не важен, swap_remove быстрее. Подробнее в уроке Векторы.

Сбор полей в цикле for по &users. for u in &users { names.push(u.name); } падает с cannot move out of `u.name` which is behind a shared reference. Исправление: names.push(u.name.clone()), или цикл for u in users, если users потом не нужен, или собери заимствования: users.iter().map(|u| u.name.as_str()).collect::<Vec<&str>>().

Вызов .unwrap() у поля-Option через ссылку. config.title.unwrap(), где config: &Config, не компилируется, потому что unwrap принимает Option по значению. Компилятор подсказывает исправление: config.title.as_ref().unwrap() (даёт &String) или config.title.as_deref() для Option<&str>.

Перемещение захваченной переменной внутри замыкания Fn. Замыкание, которое можно вызвать много раз, не может отдать то, что захватило: cannot move out of `report`, a captured variable in an `Fn` closure. Исправление: заимствуй внутри замыкания (&report), клонируй внутри него или, если оно выполняется только один раз, смени ограничение на FnOnce. Подробнее в уроке Fn, FnMut и FnOnce.

Разыменование Rc. let s: String = *shared; даёт cannot move out of an `Rc`, потому что значением могут пользоваться другие владельцы. Исправление: заимствуй его (&*shared или просто &shared), клонируй или используй Rc::unwrap_or_clone(shared). С Box иначе: им владеешь ты, поэтому let s = *boxed; работает. Подробнее в уроке Умные указатели.

  • E0382: использование перемещённого значения. Перемещать было можно, но потом ты снова использовал значение.
  • E0594: нельзя присвоить через ссылку &. То же правило «ты это только заимствовал», но для записи, а не для перемещения.
  • E0596: нельзя заимствовать как изменяемое, например когда mem::take(&mut self.x) стоит в методе, у которого есть только &self.
  • E0308: mismatched types, expected String, found &String — часто появляется сразу после перехода на заимствование. Возвращай &str или вызывай .clone().
  • E0515: нельзя вернуть ссылку на локальную переменную — когда исправление через заимствование указывает на данные, которые вот-вот удалятся.
  • Все ошибки компилятора: полный список страниц с ошибками.

Проверь себя

1. let first = names[0]; при names: Vec<String> даёт E0507. Как лучше всего исправить, если ты только выводишь first?

2. Метод fn next(&mut self) -> Option<Task> должен отдать self.pending (это Option<Task>). Что скомпилируется?

3. Почему let n = nums[0]; работает для nums: Vec<i32>, но не для Vec<String>?

Результат: 0 / 3