Ошибка 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: &Stringcannot 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` closurecannot 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 владеет памятью в куче, поэтому её можно только переместить, но не скопировать.
Представь, что ты взял у друга книжную полку на вечер. Читать любую книгу на ней можно. Но унести книгу домой нельзя: полка не твоя, и друг вернётся к пустому месту.
Почему Rust так делает
Заголовок раздела «Почему Rust так делает»У каждого значения ровно один владелец, и владелец убирает его, когда сам исчезает (см. урок Владение). Если бы можно было переместить 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 и Заимствование.
Исправление 2: клонируй
Заголовок раздела «Исправление 2: клонируй»Выбирай это, когда тебе действительно нужна своя копия, например чтобы сохранить её где-то ещё, пока оригинал остаётся на месте. Компилятор предлагает это почти в каждом сообщении 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
Что почитать
Заголовок раздела «Что почитать»- E0507 — список кодов ошибок Rust (на английском): официальное объяснение, включая исправление через
mem::replace. - Что такое владение? — книга «Язык программирования Rust» (перевод на русский): перемещения,
CloneиCopy— идеи, на которых держится эта ошибка. - Ссылки и заимствование — книга «Язык программирования Rust» (перевод на русский): что можно и чего нельзя делать через
&и&mut. std::mem::take— документация стандартной библиотеки (на английском): забрать значение из&mutи оставить значение по умолчанию; родственнаяmem::replaceпозволяет выбрать замену самому.Option— документация стандартной библиотеки (на английском):as_ref,as_derefиtakeс примерами.