Aller au contenu

Erreur Rust E0106 : missing lifetime specifier (lifetime manquant)

L’erreur E0106 veut dire : vous avez écrit une référence (&) dans le type de retour d’une fonction ou dans une struct, et Rust ne sait pas à quoi elle est empruntée. Ce n’est pas un bug dans votre logique. Le compilateur a juste besoin d’un mot de plus de votre part, ou d’un petit changement de plan. Presque tout le monde rencontre cette erreur pendant sa première semaine avec Rust, et la correction tient en général en une ligne. (Cette page fait partie de Les erreurs du compilateur Rust expliquées.)

error[E0106]: missing lifetime specifier
--> src/main.rs:1:32
|
1 | fn longer(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++

En français : « il manque un spécificateur de lifetime ». L’aide explique que le type de retour contient une valeur empruntée, mais que la signature ne dit pas si elle vient de a ou de b, et elle propose d’ajouter un lifetime nommé.

Le programme qui la provoque :

fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
println!("{}", longer("apple", "fig"));
}

Une référence est un borrowing (emprunt) : elle pointe vers des données qui appartiennent à quelqu’un d’autre. Chaque fois qu’une fonction renvoie une référence, Rust doit savoir à qui sont les données vers lesquelles elle pointe, pour vérifier qu’elles restent en vie tant que l’appelant utilise le résultat.

Regardez les trois marques sous la ligne 1. Le compilateur souligne a et b avec ----, et le type de retour avec ^. Il vous dit : « vous renvoyez un &str emprunté, et il pourrait venir de a ou de b. Lequel ? » Il ne devinera pas à votre place.

Pensez à un livre de bibliothèque. Si un ami vous tend un livre en disant « il est emprunté », vous voulez savoir à qui, pour savoir quand le rendre. Un lifetime (durée de vie), c’est cette étiquette collée sur l’emprunt.

Rust garantit qu’une référence ne peut jamais vivre plus longtemps que les données vers lesquelles elle pointe (pas de « pointeurs pendants »). Pour le vérifier lors d’un appel de fonction, le compilateur ne regarde que la signature de la fonction, pas son corps. C’est donc la signature elle-même qui doit dire d’où vient la référence renvoyée.

La plupart du temps, vous n’écrivez jamais de lifetimes, parce que Rust les complète tout seul grâce à trois règles d’élision simples :

  1. Chaque paramètre de type référence reçoit son propre lifetime.
  2. S’il y a exactement un paramètre de type référence, la référence renvoyée emprunte à celui-ci.
  3. S’il s’agit d’une méthode avec &self ou &mut self, la référence renvoyée emprunte à self.

E0106 apparaît quand aucune de ces règles ne donne de réponse : deux paramètres de type référence ou plus (et pas de self), aucun paramètre de type référence, ou une référence stockée dans une struct. La leçon Les lifetimes explique tout ça en détail, et Le borrowing présente les références elles-mêmes.

Choisissez cette solution quand la fonction renvoie vraiment un morceau de l’une de ses entrées. 'a n’est qu’un nom. L’écrire sur les entrées et sur la sortie revient à dire « le résultat vit aussi longtemps que ces entrées » :

// 'a : le &str renvoyé emprunte à a ou à b, il vit donc aussi longtemps que les deux.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let first = String::from("pomme");
let second = String::from("kiwi");
println!("Le mot le plus long : {}", longer(&first, &second));
}

Sortie :

Le mot le plus long : pomme
▶ Essayez par vous-même

Si le résultat vient toujours d’un seul paramètre, mettez 'a sur ce paramètre uniquement : fn lookup<'a>(map: &'a HashMap<String, String>, key: &str) -> Option<&'a str>. La suggestion du compilateur met 'a partout : ça fonctionne, mais c’est plus strict que nécessaire.

Choisissez cette solution quand la fonction fabrique un nouveau texte, ou quand vous ne voulez simplement pas que l’appelant reste lié à un emprunt. Une String possédée appartient à l’appelant, il n’y a donc rien à suivre :

fn greeting(name: &str, lang: &str) -> String {
if lang == "fr" {
format!("Bonjour, {name} !")
} else {
format!("Hello, {name}!")
}
}
fn main() {
println!("{}", greeting("Léa", "fr"));
println!("{}", greeting("Sam", "en"));
}

Sortie :

Bonjour, Léa !
Hello, Sam!
▶ Essayez par vous-même

Ça coûte une petite allocation mémoire. Pour la plupart des programmes, c’est tout à fait acceptable. Pas de mauvaise conscience à avoir.

Solution 3 : stocker des données possédées dans votre struct

Section intitulée « Solution 3 : stocker des données possédées dans votre struct »

Un champ de struct comme title: &str provoque la même erreur :

error[E0106]: missing lifetime specifier
--> src/main.rs:2:12
|
2 | title: &str,
| ^ expected named lifetime parameter

Ici, le compilateur attend un lifetime nommé pour la référence du champ title.

Pour les structs, la meilleure correction consiste presque toujours à posséder les données : utilisez String au lieu de &str, et Vec<T> au lieu de &[T]. La struct peut alors vivre aussi longtemps que vous le voulez :

#[derive(Debug)]
struct Book {
title: String, // possédée, pas &str
pages: u32,
}
fn main() {
let book = Book { title: String::from("Dune"), pages: 412 };
println!("{} ({} pages)", book.title, book.pages);
}

Sortie :

Dune (412 pages)
▶ Essayez par vous-même

Ne donnez un lifetime à une struct que lorsqu’elle est une vue de courte durée sur des données qui vivent ailleurs, comme un parseur qui lit un grand texte. Vous écrivez alors <'a> sur la struct et sur le champ :

struct Excerpt<'a> {
text: &'a str, // emprunte au roman ci-dessous
}
fn main() {
let novel = String::from("Appelez-moi Ismaël. Il y a quelques années...");
let first = novel.split('.').next().unwrap();
let excerpt = Excerpt { text: first };
println!("Extrait : {}", excerpt.text);
}

Sortie :

Extrait : Appelez-moi Ismaël
▶ Essayez par vous-même

Solution 4 : utiliser 'static pour les littéraux de chaîne

Section intitulée « Solution 4 : utiliser 'static pour les littéraux de chaîne »

Quand une fonction n’a aucun paramètre de type référence, l’erreur dit qu’il n’y a aucune valeur à laquelle emprunter (« there is no value for it to be borrowed from ») :

error[E0106]: missing lifetime specifier
--> src/main.rs:1:22
|
1 | fn default_name() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

Si vous ne renvoyez que des littéraux de chaîne comme "OK", écrivez &'static str. Les littéraux sont inscrits directement dans votre programme, ils vivent donc pendant toute son exécution :

fn status_text(code: u16) -> &'static str {
match code {
200 => "OK",
404 => "Not Found",
_ => "Unknown",
}
}
fn main() {
println!("200 signifie {}", status_text(200));
println!("404 signifie {}", status_text(404));
}

Sortie :

200 signifie OK
404 signifie Not Found
▶ Essayez par vous-même

(« OK » et « Not Found » sont les textes officiels des codes HTTP, c’est pourquoi ils restent en anglais.)

N’utilisez 'static que pour de vrais littéraux et des constantes. Si le texte est construit pendant l’exécution (avec format!, ou lu dans un fichier), 'static ne vous aidera pas : renvoyez une String (solution 2).

  • Les fonctions utilitaires à deux entrées, comme fn lookup(map: &HashMap<String, String>, key: &str) -> Option<&str>. Correction : liez la sortie à map seulement avec 'a (solution 1).
  • Les structs qui contiennent du texte, comme un User { name: &str } construit à partir d’une saisie analysée. Correction : utilisez des champs String (solution 3).
  • Les fonctions qui renvoient des messages fixes, comme fn error_label() -> &str. Correction : &'static str (solution 4).

Bonne nouvelle pour les méthodes : fn name(&self) -> &str n’a besoin d’aucun lifetime, grâce à la règle 3.

Petit quiz

1. Pourquoi fn first_word(s: &str) -> &str compile-t-il sans aucun lifetime ?

2. Votre struct Config { path: &str } provoque E0106. Quelle est en général la meilleure correction ?

3. Une fonction construit un message avec format! et le renvoie. Quel type de retour doit-elle avoir ?

Score : 0 / 3