Pular para o conteúdo

Erro E0507 do Rust: cannot move out of a value behind a shared reference

O E0507 quer dizer que você tentou tirar um valor (movê-lo) de algo que você só tinha pegado emprestado. Você pode olhar um valor emprestado, mas não pode levá-lo embora, porque aí o dono de verdade ficaria com um buraco. É um erro muito comum, principalmente com Vec<String>, campos de struct e Option. A correção costuma ser pegar emprestado em vez de mover, ou usar .clone(). Esta página mostra as correções primeiro e depois as explica. Para os outros erros desta série, veja Erros do compilador Rust.

error[E0507]: cannot move out of `self.name` which is behind a shared reference
--> src/main.rs:7:9
|
7 | self.name // ❌ tenta entregar uma String que `self` só pegou emprestada
| ^^^^^^^^^ 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() // ❌ tenta entregar uma String que `self` só pegou emprestada
| ++++++++

Em português: “não é possível mover para fora de self.name, que está atrás de uma referência compartilhada”. O compilador diz que o move acontece porque String não é Copy e sugere clonar o valor.

Este é o programa que gera o erro:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name // ❌ tenta entregar uma String que `self` só pegou emprestada
}
}
fn main() {
let u = User { name: String::from("Ferris") };
println!("{}", u.name());
}

O mesmo código de erro aparece com vários textos diferentes. Todos querem dizer “você só pegou isto emprestado, então não pode movê-lo”:

  • cannot move out of index of `Vec<String>` para let first = names[0];
  • cannot move out of `*r` which is behind a shared reference para let owned = *r; com r: &String
  • cannot move out of `nick` as enum variant `Some` which is behind a shared reference para match *nick { Some(n) => n, ... }
  • cannot move out of `report`, a captured variable in an `Fn` closure (uma variável capturada por uma closure Fn)
  • cannot move out of `self.log` which is behind a mutable reference (sim, nem um &mut deixa você simplesmente levar o valor)
  • cannot move out of an `Rc` para let s: String = *shared;

Olhe a linha 7. O método retorna String, um valor próprio (owned). Então self.name moveria a string para fora da struct e a entregaria para quem chamou. Mas o método só tem &self, um empréstimo compartilhado (só de leitura) do usuário. Ele não é dono do usuário, então não pode dar o nome dele para ninguém.

A parte move occurs because ... does not implement the Copy trait também importa. Se o campo fosse um u32, o Rust faria uma cópia sem dizer nada e não haveria erro. Uma String é dona de memória no heap, então só pode ser movida, não copiada.

Pense em pegar emprestada a estante de livros de um amigo por uma tarde. Você pode ler qualquer livro dela. Mas não pode levar um livro para casa: a estante não é sua, e seu amigo voltaria e encontraria um espaço vazio.

Todo valor tem exatamente um dono, e o dono faz a limpeza quando vai embora (veja Ownership, ou posse). Se você pudesse mover self.name para fora de um User emprestado, o usuário continuaria achando que é dono dessa string. Mais tarde, quando o usuário fosse descartado, ele liberaria a string uma segunda vez, ou alguém leria um campo que não existe mais. Isso é um “use after free” (uso depois de liberar) ou um “double free” (liberação dupla), dois dos piores bugs de C e C++.

Com um Vec é igual. names[0] continua sendo do vetor. Se let first = names[0]; movesse esse item para fora, o vetor ficaria com um buraco no índice 0 e tentaria liberar essa posição de novo quando fosse descartado. Por isso, acessar por índice só dá acesso, nunca ownership.

A regra numa linha: por meio de uma referência, você pode ler e (com &mut) alterar, mas não pode levar. Se você quer ownership, precisa ser dono do contêiner, ou deixar alguma coisa no lugar do valor.

Escolha esta quando você só precisa ler o valor. Na maioria das vezes, é a correção certa. Retorne &str em vez de String, escreva &v[0] em vez de v[0] e pegue emprestado o conteúdo de um Option com as_deref() ou 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 => "anônimo",
}
}
fn main() {
let users = vec![
User { name: String::from("Ferris"), nickname: Some(String::from("caranguejo")) },
User { name: String::from("Corro"), nickname: None },
];
let first = &users[0];
println!("Primeiro usuário: {}", first.name());
for user in &users {
println!("Aparece como: {}", user.display_name());
}
println!("Apelido: {}", describe(&users[1].nickname));
}

Saída:

Primeiro usuário: Ferris
Aparece como: caranguejo
Aparece como: Corro
Apelido: anônimo
▶ Experimente você mesmo

Repare em describe: o match é feito sobre nick (a referência), e não sobre *nick. Assim, n é um &String, um empréstimo, e nada é movido. Escrever match *nick { Some(n) => ... } pede uma String própria em n, e isso é o E0507. A dica do compilador diz o mesmo: consider removing the dereference here (considere remover a desreferência aqui).

as_deref() transforma um &Option<String> num Option<&str>; as_ref() dá um Option<&String>. Use esses métodos antes de .unwrap(), .map() ou match, para trabalhar com um empréstimo em vez do valor próprio. O mesmo vale para *r com r: &String: normalmente você nem precisa do *, é só usar r. Veja Option e Borrowing.

Escolha esta quando você precisa mesmo de uma cópia sua, por exemplo para guardar em outro lugar enquanto o original continua onde está. O compilador sugere isso em quase toda mensagem de 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} é o primeiro, a lista ainda tem {} nomes", names.len());
let u = User { name: String::from("Ferris") };
let mut badge = u.name();
badge.push_str(" (admin)");
println!("{badge} / {}", u.name);
}

Saída:

Ferris é o primeiro, a lista ainda tem 2 nomes
Ferris (admin) / Ferris
▶ Experimente você mesmo

Clonar um nome ou uma lista curta é barato e não tem problema nenhum, principalmente enquanto você está aprendendo. Só vira um problema com dados grandes dentro de um loop que roda muitas vezes. Se um empréstimo (Correção 1) bastar, prefira ele.

Escolha esta quando você tem acesso &mut e quer mover o valor para fora de verdade, por exemplo para entregar um log pronto ou a próxima tarefa. Você não pode deixar um buraco, mas pode colocar um substituto:

  • std::mem::take(&mut x) pega o valor e deixa o valor padrão no lugar ("" para uma String, vazio para um Vec).
  • option.take() pega o que está dentro de um Option e deixa None.
  • std::mem::replace(&mut x, new) coloca new no lugar e devolve o valor antigo.
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("build ok"),
next_step: Some(String::from("deploy")),
status: Status::Queued,
};
let old = job.set_status(Status::Running);
println!("Status: {old:?} -> {:?}", job.status);
println!("Próximo passo: {:?}", job.start_next());
println!("Próximo passo de novo: {:?}", job.start_next());
println!("Log: {:?}", job.take_log());
println!("Log agora: {:?}", job.log);
}

Saída:

Status: Queued -> Running
Próximo passo: Some("deploy")
Próximo passo de novo: None
Log: "build ok"
Log agora: ""
▶ Experimente você mesmo

A struct nunca fica com um buraco: depois de take_log, o log é uma string vazia; depois de start_next, o passo é None. Esta é a correção padrão para “cannot move out of self.x which is behind a mutable reference”. O mem::take exige que o tipo implemente Default; para outros tipos, use mem::replace.

Correção 4: Seja dono da coleção e depois mova para fora dela

Seção intitulada “Correção 4: Seja dono da coleção e depois mova para fora dela”

Escolha esta quando você já terminou de usar a coleção ou a struct e quer o conteúdo dela. Se você é dono do Vec, ele pode entregar itens: remove(i), swap_remove(i), pop() ou into_iter() para todos de uma vez. Um método que recebe self (e não &self) é dono da struct e pode mover os campos dela para fora:

struct User {
name: String,
}
impl User {
fn into_name(self) -> String {
self.name
}
}
fn main() {
let mut queue = vec![String::from("email"), String::from("backup"), String::from("report")];
let first = queue.remove(0);
println!("Fazendo {first}, restam: {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:?}");
}

Saída:

Fazendo email, restam: ["backup", "report"]
["Ferris", "Corro"]
▶ Experimente você mesmo

into_iter() consome o vetor, então você não pode usar users depois (isso seria o E0382). O prefixo into_ em into_name é um costume de nomes do Rust: ele avisa quem lê que “isto consome o valor”. remove(0) puxa todos os outros itens uma posição para trás; se a ordem não importa, swap_remove é mais rápido. Veja Removendo elementos.

Juntar campos num loop for sobre &users. for u in &users { names.push(u.name); } falha com cannot move out of `u.name` which is behind a shared reference. Correção: names.push(u.name.clone()), ou percorra com for u in users se não precisar de users depois, ou junte empréstimos: users.iter().map(|u| u.name.as_str()).collect::<Vec<&str>>().

Chamar .unwrap() num campo Option por meio de uma referência. config.title.unwrap() com config: &Config falha, porque unwrap recebe o Option por valor. O compilador sugere a correção: config.title.as_ref().unwrap() (dá um &String), ou config.title.as_deref() para um Option<&str>.

Mover uma variável capturada dentro de uma closure Fn. Uma closure que pode ser chamada muitas vezes não pode entregar o que capturou: cannot move out of `report`, a captured variable in an `Fn` closure. Correção: pegue emprestado dentro da closure (&report), clone dentro dela ou, se ela só roda uma vez, troque o bound para FnOnce. Veja Fn, FnMut e FnOnce.

Desreferenciar um Rc. let s: String = *shared; dá cannot move out of an `Rc`, porque outros donos ainda podem estar usando o valor. Correção: pegue emprestado (&*shared ou só &shared), clone, ou use Rc::unwrap_or_clone(shared). Um Box é diferente: você é dono dele, então let s = *boxed; funciona. Veja Smart pointers.

  • E0382: uso de um valor movido. Você podia mover, mas usou o valor de novo depois.
  • E0594: não é possível atribuir por meio de uma referência &. A mesma regra de “você só pegou emprestado”, mas para escrever em vez de mover.
  • E0596: não é possível pegar emprestado como mutável, por exemplo quando mem::take(&mut self.x) é usado num método que só tem &self.
  • E0308: tipos incompatíveis, expected String, found &String, que costuma aparecer logo depois de trocar para um empréstimo. Retorne &str ou chame .clone().
  • E0515: não é possível retornar uma referência para uma variável local, quando uma correção com empréstimo aponta para dados que estão prestes a ser descartados.
  • Todos os erros do compilador: a lista completa de páginas de erro.

Teste o que aprendeu

1. let first = names[0]; com names: Vec<String> dá E0507. Qual costuma ser a melhor correção se você só vai imprimir first?

2. Um método fn next(&mut self) -> Option<Task> deve entregar self.pending (um Option<Task>). O que compila?

3. Por que let n = nums[0]; funciona com nums: Vec<i32>, mas não com um Vec<String>?

Pontuação: 0 / 3