Aller au contenu

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.

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>` pour let first = names[0];
  • cannot move out of `*r` which is behind a shared reference pour let owned = *r; avec r: &String
  • cannot move out of `nick` as enum variant `Some` which is behind a shared reference pour 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 (eh oui, même &mut ne vous laisse pas simplement prendre la valeur)
  • cannot move out of an `Rc` pour let s: String = *shared;

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.

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.

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 : Ferris
Affiché comme : crabe
Affiché comme : Corro
Surnom : anonyme
▶ Essayez par vous-même

Regardez 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.

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 noms
Ferris (admin) / Ferris
▶ Essayez par vous-même

Cloner 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 une String, vide pour un Vec).
  • option.take() prend ce qu’il y a dans une Option et laisse None.
  • std::mem::replace(&mut x, new) met new à 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 : None
Journal : "build ok"
Journal maintenant : ""
▶ Essayez par vous-même

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"]
▶ Essayez par vous-même

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.

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.

  • 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

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