Erreur Rust E0507 : cannot move out of a value behind a shared reference
E0507 veut dire que vous avez essayé de prendre une valeur (de la déplacer) hors de quelque chose que vous aviez seulement emprunté. Vous pouvez regarder une valeur empruntée, mais pas l’emporter, sinon le vrai propriétaire se retrouverait avec un trou. C’est une erreur très courante, surtout avec Vec<String>, les champs de struct et Option. La correction consiste généralement à emprunter à la place, ou à faire un .clone(). Cette page vous montre d’abord les solutions, puis les explique. Pour les autres erreurs de cette série, consultez les erreurs du compilateur Rust.
Le message d’erreur
Section intitulée « Le message d’erreur »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 | ++++++++Traduit, le compilateur dit : « impossible de déplacer self.name, qui se trouve derrière une référence partagée ». Le déplacement a lieu parce que String n’implémente pas Copy, et l’aide propose de cloner la valeur.
Voici le programme qui la provoque :
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());}(Le commentaire dit : on essaie de donner une String que self ne fait qu’emprunter.)
Le même code d’erreur existe avec plusieurs formulations. Elles veulent toutes dire « vous avez seulement emprunté ceci, vous ne pouvez donc pas le déplacer » :
cannot move out of index of `Vec<String>`pourlet first = names[0];cannot move out of `*r` which is behind a shared referencepourlet owned = *r;avecr: &Stringcannot move out of `nick` as enum variant `Some` which is behind a shared referencepourmatch *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(eh oui, même&mutne vous laisse pas simplement prendre la valeur)cannot move out of an `Rc`pourlet s: String = *shared;
Ce que ça veut dire
Section intitulée « Ce que ça veut dire »Regardez la ligne 7. La méthode renvoie une String, une valeur possédée. self.name déplacerait donc la chaîne hors de la struct pour la donner à l’appelant. Mais la méthode n’a que &self, un emprunt partagé (en lecture seule) de l’utilisateur. Elle ne possède pas l’utilisateur, elle ne peut donc pas donner son nom.
La partie move occurs because ... does not implement the Copy trait compte aussi. Si le champ était un u32, Rust le copierait discrètement et il n’y aurait pas d’erreur. Une String possède de la mémoire sur le tas, elle ne peut donc être que déplacée, pas copiée.
Imaginez que vous empruntez la bibliothèque d’un ami pour l’après-midi. Vous pouvez lire n’importe quel livre dessus. Mais vous ne pouvez pas rapporter un livre chez vous : l’étagère n’est pas à vous, et votre ami retrouverait une place vide.
Pourquoi Rust fait ça
Section intitulée « Pourquoi Rust fait ça »Chaque valeur a exactement un propriétaire, et ce propriétaire la nettoie quand il disparaît (voir L’ownership, la possession). Si vous pouviez déplacer self.name hors d’un User emprunté, l’utilisateur croirait toujours posséder cette chaîne. Plus tard, l’utilisateur serait libéré, et il libérerait la chaîne une deuxième fois, ou quelqu’un lirait un champ qui n’existe plus. C’est un « use after free » ou un « double free », deux des pires bugs en C et en C++.
C’est pareil pour un Vec. names[0] appartient toujours au vecteur. Si let first = names[0]; le déplaçait, le vecteur aurait un trou à l’indice 0, et il essaierait de libérer cette case une deuxième fois au moment d’être libéré. L’indexation vous donne donc un accès, jamais la possession.
La règle en une ligne : à travers une référence, vous pouvez lire et (avec &mut) modifier, mais pas prendre. Si vous voulez la possession, vous devez posséder le conteneur, ou laisser quelque chose à la place de la valeur.
Comment corriger
Section intitulée « Comment corriger »Solution 1 : emprunter au lieu de déplacer
Section intitulée « Solution 1 : emprunter au lieu de déplacer »Choisissez-la quand vous avez seulement besoin de lire la valeur. C’est la bonne solution la plupart du temps. Renvoyez &str au lieu de String, écrivez &v[0] au lieu de v[0], et empruntez le contenu d’une Option avec 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 => "anonyme", }}
fn main() { let users = vec![ User { name: String::from("Ferris"), nickname: Some(String::from("crabe")) }, User { name: String::from("Corro"), nickname: None }, ]; let first = &users[0]; println!("Premier utilisateur : {}", first.name()); for user in &users { println!("Affiché comme : {}", user.display_name()); } println!("Surnom : {}", describe(&users[1].nickname));}Sortie :
Premier utilisateur : FerrisAffiché comme : crabeAffiché comme : CorroSurnom : anonymeRegardez describe : elle fait le match sur nick (la référence), pas sur *nick. Du coup, n est une &String, un emprunt, et rien n’est déplacé. Écrire match *nick { Some(n) => ... } demande une String possédée dans n, et c’est E0507. L’indice du compilateur le dit aussi : consider removing the dereference here (essayez d’enlever le déréférencement ici).
as_deref() transforme une &Option<String> en Option<&str> ; as_ref() donne une Option<&String>. Utilisez-les avant .unwrap(), .map() ou match, pour travailler avec un emprunt plutôt qu’avec la valeur possédée. C’est pareil pour *r avec r: &String : vous n’avez généralement pas besoin du *, utilisez simplement r. Voir Option et Le borrowing.
Solution 2 : cloner la valeur
Section intitulée « Solution 2 : cloner la valeur »Choisissez-la quand vous avez vraiment besoin de votre propre copie, par exemple pour la ranger ailleurs pendant que l’original reste à sa place. Le compilateur la propose dans presque tous les messages 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} est en premier, la liste contient toujours {} noms", names.len());
let u = User { name: String::from("Ferris") }; let mut badge = u.name(); badge.push_str(" (admin)"); println!("{badge} / {}", u.name);}Sortie :
Ferris est en premier, la liste contient toujours 2 nomsFerris (admin) / FerrisCloner un nom ou une petite liste ne coûte presque rien, et c’est tout à fait acceptable, surtout pendant que vous apprenez. Ça ne devient un problème que pour de grosses données dans une boucle très sollicitée. Si un emprunt (solution 1) suffit, préférez-le.
Solution 3 : sortir la valeur et laisser quelque chose à sa place
Section intitulée « Solution 3 : sortir la valeur et laisser quelque chose à sa place »Choisissez-la quand vous avez un accès &mut et que vous voulez vraiment déplacer la valeur, par exemple pour transmettre un journal terminé ou la tâche suivante. Vous ne pouvez pas laisser de trou, mais vous pouvez mettre un remplaçant :
std::mem::take(&mut x)prend la valeur et laisse sa valeur par défaut (""pour uneString, vide pour unVec).option.take()prend ce qu’il y a dans uneOptionet laisseNone.std::mem::replace(&mut x, new)metnewà la place et vous rend l’ancienne valeur.
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!("Statut : {old:?} -> {:?}", job.status); println!("Étape suivante : {:?}", job.start_next()); println!("Étape suivante, encore : {:?}", job.start_next()); println!("Journal : {:?}", job.take_log()); println!("Journal maintenant : {:?}", job.log);}Sortie :
Statut : Queued -> RunningÉtape suivante : Some("deploy")Étape suivante, encore : NoneJournal : "build ok"Journal maintenant : ""La struct ne se retrouve jamais avec un trou : après take_log, le journal est une chaîne vide ; après start_next, l’étape vaut None. C’est la correction classique pour « cannot move out of self.x which is behind a mutable reference ». mem::take exige que le type implémente Default ; pour les autres types, utilisez mem::replace.
Solution 4 : posséder la collection, puis en sortir les valeurs
Section intitulée « Solution 4 : posséder la collection, puis en sortir les valeurs »Choisissez-la quand vous avez fini avec la collection ou la struct et que vous voulez son contenu. Si c’est vous qui possédez le Vec, il peut vous céder ses éléments : remove(i), swap_remove(i), pop(), ou into_iter() pour tous. Une méthode qui prend self (et non &self) possède la struct et peut en déplacer les champs :
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!("En cours : {first}, il reste : {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:?}");}Sortie :
En cours : email, il reste : ["backup", "report"]["Ferris", "Corro"]into_iter() consomme le vecteur, vous ne pouvez donc plus utiliser users ensuite (ce serait E0382). Le préfixe into_ de into_name est une habitude de nommage en Rust : il indique au lecteur que « cette méthode consomme la valeur ». remove(0) décale tous les autres éléments ; si l’ordre n’a pas d’importance, swap_remove est plus rapide. Voir Retirer des éléments.
Où vous le verrez souvent
Section intitulée « Où vous le verrez souvent »Rassembler des champs dans une boucle for sur &users. for u in &users { names.push(u.name); } échoue avec cannot move out of `u.name` which is behind a shared reference. Correction : names.push(u.name.clone()), ou bouclez avec for u in users si vous n’avez plus besoin de users ensuite, ou rassemblez des emprunts : users.iter().map(|u| u.name.as_str()).collect::<Vec<&str>>().
Appeler .unwrap() sur un champ Option à travers une référence. config.title.unwrap() avec config: &Config échoue, parce que unwrap prend l’Option par valeur. Le compilateur propose la correction : config.title.as_ref().unwrap() (qui donne une &String), ou config.title.as_deref() pour une Option<&str>.
Déplacer une variable capturée dans une closure Fn. Une closure qui peut être appelée plusieurs fois ne peut pas donner ce qu’elle a capturé : cannot move out of `report`, a captured variable in an `Fn` closure. Correction : empruntez dans la closure (&report), clonez à l’intérieur ou, si elle ne s’exécute qu’une fois, changez la contrainte en FnOnce. Voir Fn, FnMut et FnOnce.
Déréférencer un Rc. let s: String = *shared; donne cannot move out of an `Rc`, car d’autres propriétaires utilisent peut-être encore la valeur. Correction : empruntez-la (&*shared ou simplement &shared), clonez-la, ou utilisez Rc::unwrap_or_clone(shared). Un Box, c’est différent : vous le possédez, donc let s = *boxed; fonctionne. Voir Les pointeurs intelligents.
Erreurs liées
Section intitulée « Erreurs liées »- E0382 : utilisation d’une valeur déplacée. Vous aviez le droit de déplacer, mais vous avez réutilisé la valeur ensuite.
- E0594 : impossible d’affecter à travers une référence
&. La même règle « vous avez seulement emprunté », mais pour écrire au lieu de déplacer. - E0596 : impossible d’emprunter en mutable, par exemple quand
mem::take(&mut self.x)est utilisé dans une méthode qui n’a que&self. - E0308 : types incompatibles,
expected String, found &String, qui apparaît souvent juste après être passé à un emprunt. Renvoyez&str, ou appelez.clone(). - E0515 : impossible de renvoyer une référence vers une variable locale, quand une correction par emprunt pointe vers des données sur le point d’être libérées.
- Toutes les erreurs du compilateur : la liste complète des pages d’erreurs.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. let first = names[0]; avec names: Vec<String> donne E0507. Quelle est généralement la meilleure correction si vous ne faites qu'afficher first ?
2. Une méthode fn next(&mut self) -> Option<Task> doit céder self.pending (une Option<Task>). Qu'est-ce qui compile ?
3. Pourquoi let n = nums[0]; fonctionne-t-il avec nums: Vec<i32>, mais pas avec un Vec<String> ?
Score : 0 / 3
Pour aller plus loin
Section intitulée « Pour aller plus loin »- E0507 — Rust error codes index (en anglais) : l’explication officielle, avec la correction par
mem::replace. - What Is Ownership? — The Rust Book (en anglais) : les déplacements,
CloneetCopy, les idées derrière cette erreur. - References and Borrowing — The Rust Book (en anglais) : ce que vous pouvez faire, ou non, à travers
&et&mut. std::mem::take— documentation de la bibliothèque standard (en anglais) : sortir une valeur d’un&muten laissant sa valeur par défaut ; sa cousinemem::replacevous laisse choisir le remplaçant.Option— documentation de la bibliothèque standard (en anglais) :as_ref,as_derefettake, avec des exemples.