Aller au contenu

Les lifetimes en Rust

Les lifetimes (durées de vie) sont souvent le premier vrai mur que rencontrent les nouveaux développeurs Rust. Cette page reste volontairement douce : le but est simplement de reconnaître ce qu’est une lifetime et de comprendre pourquoi le compilateur vous en demande une, pas d’en maîtriser toutes les règles.

Vous savez déjà, grâce au borrowing, qu’une référence (&) permet d’utiliser une valeur sans la posséder. Mais une référence n’est valide que tant que la valeur vers laquelle elle pointe existe encore. Rust doit s’assurer que vous ne vous retrouvez jamais avec une référence vers quelque chose qui a déjà été libéré. Ce type de bug s’appelle une référence pendante (dangling reference), et c’est une source classique de plantages dans des langages comme le C.

La plupart du temps, Rust s’en sort tout seul en regardant les portées de votre code, et vous n’avez jamais à y penser (c’est exactement ce que vous avez vu sur la page La portée). Mais parfois, surtout avec les fonctions, le compilateur n’arrive pas à le déterminer seul et vous demande de l’écrire explicitement. C’est cela, une annotation de lifetime.

Voici une fonction qui renvoie la plus longue de deux slices de chaîne :

fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}

Elle ne compile pas. L’erreur ressemble à ceci :

error[E0106]: missing lifetime specifier

Autrement dit (rustc s’exprime toujours en anglais) : il manque une indication de lifetime.

Le problème : la référence renvoyée peut être empruntée à a ou à b, et Rust ne peut pas savoir laquelle rien qu’en lisant la fonction. Il ne peut donc pas garantir que la référence que vous récupérez sera encore valide au moment où vous l’utiliserez.

Pour corriger cela, vous donnez un nom à une lifetime (par convention, une lettre minuscule comme 'a) et vous indiquez à Rust que la lifetime de la valeur de retour est liée aux deux paramètres :

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("bonjour");
let s2 = String::from("salut");
let result = longest(&s1, &s2);
println!("La plus longue : {result}");
}

Sortie :

La plus longue : bonjour
▶ Essayez par vous-même

Lisez 'a comme une étiquette, pas comme une valeur : elle ne fixe aucune durée réelle. Elle dit simplement à Rust « ces références doivent toutes rester valides au moins sur la même portion de code ». Le compilateur vérifie ensuite, à chaque appel, que c’est bien le cas.

Sans cette annotation, le compilateur ne peut vraiment pas savoir si la référence renvoyée par longest risque de survivre à l’une des chaînes dont elle provient. C’est l’annotation de lifetime qui lui permet de prouver, à la compilation et sans aucun coût à l’exécution, que la référence obtenue sera toujours utilisable sans danger.

Le prochain endroit où vous croiserez des lifetimes, ce sont les structs. Jusqu’ici, chaque champ de struct possédait ses données, comme une String. Un champ peut aussi être une référence, mais alors la struct n’est valide que tant que les données vers lesquelles elle pointe existent. Rust vous oblige à l’écrire avec une lifetime :

struct Highlight<'a> {
text: &'a str, // emprunte à une String qui vit ailleurs
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("Passage : {}", first.text);
println!("Le livre entier fait {} octets", book.len());
}

Sortie :

Passage : Call me Ishmael
Le livre entier fait 34 octets
▶ Essayez par vous-même

Le texte du livre (la première phrase de Moby Dick) reste en anglais : l’exemple découpe des octets, et une lettre accentuée en occupe deux, ce qui fausserait le découpage et le nombre d’octets.

Lisez Highlight<'a> comme « un Highlight qui emprunte quelque chose pendant la lifetime 'a ». La partie &book[..15] est une slice : elle emprunte les 15 premiers octets de book sans les copier. Comme first ne fait qu’emprunter, book reste utilisable ensuite.

C’est la lifetime qui permet à Rust de repérer l’erreur suivante, où la String est libérée alors que la struct pointe encore dedans :

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // book est libéré ici...
println!("{}", highlight.text); // ...mais highlight pointe encore dedans
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // book est libéré ici...
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ...mais highlight pointe encore dedans
| -------------- borrow later used here

Le message dit que book ne vit pas assez longtemps : il est libéré à la ligne 10 alors qu’il est encore emprunté, puis l’emprunt est utilisé à la ligne 12.

Un conseil pratique : si les lifetimes d’une struct vous gênent, stockez une String possédée au lieu d’un &str. Le texte est copié, mais la struct ne dépend plus de rien d’autre, et la lifetime disparaît. Beaucoup de développeurs Rust expérimentés commencent ainsi, et ne passent aux références que lorsqu’ils ont une bonne raison de le faire.

Vous écrirez 'a bien moins souvent que cette page ne le laisse croire. Une grande partie du code Rust n’a jamais besoin de lifetime explicite, car le compilateur sait les déduire dans la plupart des situations courantes. Vous les rencontrerez surtout quand une fonction renvoie une référence et que le compilateur ne peut pas deviner seul d’où elle vient. À ce moment-là, souvenez-vous : ce n’est pas un bug, c’est le compilateur qui vous demande de confirmer quelque chose qu’il ne peut pas prouver tout seul.

Écrivez une fonction shortest qui prend deux slices de chaîne et renvoie la plus courte. Si elles ont la même longueur, renvoyez la première. Appelez-la avec "girafe" et "chat".

Indice : elle a la même forme que longest ci-dessus. Il vous faut <'a> après le nom, et 'a sur les deux paramètres et sur le type de retour.

Voir la solution
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("girafe");
let small = String::from("chat");
println!("La plus courte : {}", shortest(&big, &small));
println!("En cas d'égalité, la première gagne : {}", shortest("loup", "ours"));
}

Sortie :

La plus courte : chat
En cas d'égalité, la première gagne : loup
▶ Essayez par vous-même

La slice renvoyée peut venir de a ou de b : Rust a donc besoin de 'a pour savoir que le résultat n’est valide que tant que les deux entrées le sont. Utiliser <= au lieu de < fait qu’en cas d’égalité, c’est a qui est choisie. (Les mots choisis n’ont pas d’accent, car len() compte des octets.)

Créez une struct Quote avec deux champs &str, text et author. Créez-en une dont text emprunte les 20 premiers octets de la String "Fearless concurrency is a goal of Rust", et dont author est le littéral "The Rust Book". Affichez-la sous la forme Fearless concurrency (The Rust Book).

Indice : comme Highlight plus haut, la struct a besoin d’une lifetime : struct Quote<'a>, avec &'a str pour les deux champs. &speech[..20] est une slice des 20 premiers octets.

Voir la solution
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "The Rust Book",
};
println!("{} ({})", quote.text, quote.author);
}

Sortie :

Fearless concurrency (The Rust Book)
▶ Essayez par vous-même

quote emprunte à speech, elle ne peut donc pas lui survivre. Le littéral "The Rust Book" est intégré au programme et vit pendant toute son exécution : il dure donc largement assez longtemps pour 'a. Essayez de créer speech dans un bloc { } intérieur et d’afficher la citation après la fin du bloc, comme dans l’exemple d’erreur plus haut : vous verrez le compilateur vous arrêter.

  • Une lifetime indique au compilateur comment les références sont liées entre elles, pour qu’il puisse exclure les références pendantes.
  • Vous n’avez besoin d’en écrire une que lorsque le compilateur ne peut pas la déduire seul, le plus souvent dans une fonction qui renvoie une référence.
  • 'a ne fixe pas une durée : elle relie les lifetimes de plusieurs références pour que le compilateur puisse les vérifier.
  • Une struct qui contient une référence a aussi besoin d’une lifetime (struct Highlight<'a>), et ne peut pas survivre aux données qu’elle emprunte. Stocker une String possédée évite ce problème.
  • Il s’agit uniquement d’une vérification à la compilation ; elle n’ajoute aucun coût à l’exécution.

Petit quiz

1. Quel problème les lifetimes résolvent-elles ?

2. Pourquoi fn longest(a: &str, b: &str) -> &str ne compilait-elle pas sans annotation de lifetime ?

3. Ajouter une annotation de lifetime coûte-t-il quelque chose à l'exécution ?

4. D'après cette page, quand avez-vous le plus souvent besoin d'écrire une lifetime explicite ?

Score : 0 / 4