9. Durées de vie
Exemple complet : examples/l09_lifetimes.rs — cargo run --example l09_lifetimes.
Le problème que cache un ramasse-miettes
Section intitulée « Le problème que cache un ramasse-miettes »En C# ou en Java, une référence maintient son objet en vie : tant que vous pouvez l’atteindre, le ramasse-miettes ne le libère pas. Une « référence pendante » ne peut tout simplement pas exister.
Rust n’a pas de ramasse-miettes. Une valeur est libérée quand son propriétaire sort de la portée (leçon 3), donc le compilateur doit prouver qu’aucune référence n’est encore utilisée à ce moment-là :
let r;{ let s = String::from("hello"); r = &s;}println!("{r}");error[E0597]: `s` does not live long enough --> e09_dangling.rs:5:13 |4 | let s = String::from("hello"); | - binding `s` declared here5 | r = &s; | ^^ borrowed value does not live long enough6 | } | - `s` dropped here while still borrowed7 | println!("{r}"); | - borrow later used hereL’intervalle pendant lequel une référence est valide est sa durée de vie (lifetime). À l’intérieur d’une seule fonction, le compilateur déduit les durées de vie tout seul — vous comptez dessus depuis la leçon 4. Vous ne les écrivez que lorsqu’une référence franchit une frontière de fonction ou de struct et que le compilateur ne peut pas deviner la relation.
Annoter une fonction
Section intitulée « Annoter une fonction »De quelle entrée le résultat est-il emprunté ?
fn longest(a: &str, b: &str) -> &str { if a.len() >= b.len() { a } else { b }}error[E0106]: missing lifetime specifier --> e09_longest.rs:1:33 |1 | fn longest(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 longest<'a>(a: &'a str, b: &'a str) -> &'a str { | ++++ ++ ++ ++Le compilateur vérifie chaque fonction sur sa seule signature, jamais sur son corps — de la même façon qu’un appelant C# ne voit que la déclaration d’une méthode. La signature doit donc le dire :
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() >= b.len() { a } else { b }}Lisez <'a> comme un paramètre générique (il est déclaré au même endroit que <T>) : « pour une certaine durée de vie 'a pendant laquelle a et b sont tous deux valides, le résultat est lui aussi valide pendant 'a ». En pratique, 'a devient la plus courte des deux, si bien que l’appelant ne peut pas garder le résultat plus longtemps que l’une ou l’autre des entrées :
let title = String::from("Rust for C# developers");let winner;{ let subtitle = String::from("ownership"); winner = longest(&title, &subtitle);}println!("{winner}");error[E0597]: `subtitle` does not live long enough --> e09_longest_scope.rs:10:34 | 9 | let subtitle = String::from("ownership"); | -------- binding `subtitle` declared here10 | winner = longest(&title, &subtitle); | ^^^^^^^^^ borrowed value does not live long enough11 | } | - `subtitle` dropped here while still borrowed12 | println!("{winner}"); | ------ borrow later used hereÀ l’exécution, title est la chaîne la plus longue, donc cela fonctionnerait par hasard — mais la signature ne promet rien sur celle qui est renvoyée, et le compilateur vous tient à la signature.
N’annotez que ce que le résultat emprunte réellement. Ici, le résultat vient de a seul, donc len_from n’a besoin d’aucune durée de vie et peut être n’importe quoi d’éphémère :
fn prefix_of<'a>(a: &'a str, len_from: &str) -> &'a str { &a[..len_from.len().min(a.len())]}L’élision : quand on peut les omettre
Section intitulée « L’élision : quand on peut les omettre »La plupart des fonctions ne mentionnent jamais de durée de vie, parce que trois règles d’élision les complètent :
- Chaque paramètre référence reçoit sa propre durée de vie.
- S’il y a exactement une durée de vie en entrée, elle est utilisée pour toutes les références en sortie.
- Si l’un des paramètres est
&selfou&mut self, c’est sa durée de vie qui est utilisée pour les sorties.
fn first_word(text: &str) -> &str { // règle 2 : le résultat emprunte `text` text.split_whitespace().next().unwrap_or("")}longest avait besoin d’une annotation parce qu’elle a deux entrées et pas de self : aucune règle ne s’applique.
L’élision ne fait jamais compiler un programme incorrect. Quand les règles produisent une durée de vie qui ne convient pas, vous obtenez quand même une erreur — et renvoyer une référence vers une variable locale est toujours une erreur, quelle que soit l’annotation :
fn shout(word: &str) -> &str { let upper = word.to_uppercase(); &upper}error[E0515]: cannot return reference to local variable `upper` --> e09_local.rs:3:5 |3 | &upper | ^^^^^^ returns a reference to data owned by the current functionLa correction est celle de la leçon 4 : renvoyer la String possédée.
Des structs qui empruntent
Section intitulée « Des structs qui empruntent »Une struct qui contient une référence doit déclarer la durée de vie, exactement comme un paramètre de type générique :
struct Excerpt { text: &str,}error[E0106]: missing lifetime specifier --> e09_struct.rs:2:11 |2 | text: &str, | ^ expected named lifetime parameter |help: consider introducing a named lifetime parameter |1 ~ struct Excerpt<'a> {2 ~ text: &'a str, |struct Excerpt<'a> { text: &'a str,}
impl Excerpt<'_> { // '_ : « une certaine durée de vie, je n'ai pas besoin de son nom » fn word_count(&self) -> usize { self.text.split_whitespace().count() }}
let novel = String::from("Call me Ishmael. Some years ago, never mind how long precisely...");let excerpt = Excerpt { text: novel.split('.').next().unwrap_or("") };// excerpt: "Call me Ishmael" (3 words)Excerpt<'a> se lit « un extrait qui ne peut pas survivre au texte dans lequel il pointe ». Le compilateur y veille :
error[E0597]: `novel` does not live long enough --> e09_struct_outlive.rs:9:35 | 8 | let novel = String::from("Call me Ishmael. Some years ago..."); | ----- binding `novel` declared here 9 | excerpt = Excerpt { text: novel.split('.').next().unwrap() }; | ^^^^^ borrowed value does not live long enough10 | } | - `novel` dropped here while still borrowed11 | println!("{}", excerpt.text); | ------------ borrow later used hereL’équivalent C# le plus proche est une ref struct comme Span<T> : elle peut pointer dans la mémoire de quelqu’un d’autre, donc le compilateur restreint les endroits où elle peut aller. En Rust, n’importe quelle struct peut être ainsi.
La durée de vie dans une signature de méthode compte
Section intitulée « La durée de vie dans une signature de méthode compte »Un tokenizer qui renvoie des slices de son entrée :
struct Parser<'a> { input: &'a str, pos: usize,}
impl<'a> Parser<'a> { fn next_token(&mut self) -> Option<&'a str> { // … renvoie &self.input[start..end] }}
fn tokenize(line: &str) -> Vec<&str> { let mut parser = Parser::new(line); let mut tokens = Vec::new(); while let Some(token) = parser.next_token() { tokens.push(token); } tokens // le parser est libéré, les tokens survivent}// tokens: ["let", "x", "=", "42"]Le type de retour indique &'a str : un token emprunte le texte d’entrée, pas le parser. S’il avait été écrit Option<&str>, la règle d’élision 3 lierait chaque token à &mut self — et vous ne pourriez pas garder deux tokens à la fois (exercice 3).
'static est la durée de vie des données valides pendant toute l’exécution du programme. Les littéraux de chaîne sont stockés dans le binaire, ils l’ont donc :
fn default_greeting() -> &'static str { "hello"}Vous rencontrerez surtout 'static comme contrainte (bound), par exemple sur std::thread::spawn : un nouveau thread peut survivre à la fonction qui l’a démarré, il ne peut donc pas emprunter les variables locales de cette fonction.
let name = String::from("worker");let label: &str = &name;let handle = std::thread::spawn(move || println!("{label}"));handle.join().unwrap();error[E0597]: `name` does not live long enough --> e09_static.rs:4:23 | 3 | let name = String::from("worker"); | ---- binding `name` declared here 4 | let label: &str = &name; | ^^^^^ borrowed value does not live long enough 5 | let h = thread::spawn(move || println!("{label}")); | ------------------------------------------ argument requires that `name` is borrowed for `'static` 6 | h.join().unwrap(); 7 | } | - `name` dropped here while still borrowed |note: requirement that the value outlives `'static` introduced hereDéplacer la String elle-même dans la closure corrige le problème. T: 'static ne signifie pas « vit pour toujours » : cela signifie « ne contient aucune donnée empruntée susceptible d’expirer » — une String possédée remplit la condition. Les threads sont l’objet de la leçon 12.
Quand les durées de vie gênent : possédez les données
Section intitulée « Quand les durées de vie gênent : possédez les données »En venant de C#, le réflexe est de stocker des références partout. En Rust, une struct pleine de champs &'a propage sa durée de vie à tout ce qui la contient. Une règle pratique pendant l’apprentissage :
| Situation | Utilisez |
|---|---|
| Paramètres de fonction que vous ne faites que lire | &str, &[T], &T |
| Vues éphémères sur des données que possède quelqu’un d’autre (parsers, itérateurs, extraits) | une struct avec 'a |
| Données qu’une struct garde longtemps | String, Vec<T>, T possédés |
| Données partagées par plusieurs propriétaires | Rc/Arc — leçon 11 |
Cloner quelques chaînes pour éviter un paramètre de durée de vie est un compromis parfaitement acceptable.
À retenir
Section intitulée « À retenir »- Une durée de vie est l’intervalle pendant lequel une référence est valide ; le compilateur vérifie qu’aucune référence ne survit à sa valeur.
- Les annotations décrivent les relations entre références dans une signature ; elles ne prolongent jamais la vie d’une valeur.
- Trois règles d’élision couvrent la plupart des fonctions ; vous annotez quand il y a plusieurs entrées et pas de
self. - Une struct qui contient une référence porte un paramètre de durée de vie et ne peut pas survivre à ce qu’elle emprunte.
- Les données
'staticne contiennent aucun emprunt qui expire ; des API commethread::spawnl’exigent. - En cas de doute, possédez les données.
Exercices
Section intitulée « Exercices »- Écrivez
fn longest_line(text: &str) -> &strqui renvoie la ligne la plus longue d’un texte. A-t-elle besoin d’une annotation de durée de vie ? Écrivez ensuitefn pick(first: &str, second: &str, use_first: bool) -> &str— de quoi a-t-elle besoin ?
Solution
fn longest_line(text: &str) -> &str { text.lines().max_by_key(|line| line.len()).unwrap_or("")}
fn pick<'a>(first: &'a str, second: &'a str, use_first: bool) -> &'a str { if use_first { first } else { second }}
let poem = String::from("short\na much longer line\nmid");assert_eq!(longest_line(&poem), "a much longer line");assert_eq!(pick("left", "right", false), "right");longest_line a une seule référence en entrée, donc la règle d’élision 2 s’applique. pick en a deux et peut renvoyer l’une ou l’autre, donc les deux doivent partager 'a — exactement comme longest.
- Définissez
struct Highlight<'a> { line: &'a str, column: usize }et écrivezfind_highlights(text, word)qui renvoie chaque ligne detextcontenantword. L’appelant doit pouvoir chercher avec une requêteStringtemporaire, libérée avant que les résultats soient utilisés.
Solution
#[derive(Debug, PartialEq)]struct Highlight<'a> { line: &'a str, column: usize,}
fn find_highlights<'a>(text: &'a str, word: &str) -> Vec<Highlight<'a>> { text.lines() .filter_map(|line| line.find(word).map(|column| Highlight { line, column })) .collect()}
let text = String::from("I like Rust\nC# too\nRust again");let hits = { let query = String::from("Rust"); // libérée à la fin de ce bloc find_highlights(&text, &query)};assert_eq!(hits, [Highlight { line: "I like Rust", column: 7 }, Highlight { line: "Rust again", column: 0 }]);Les résultats n’empruntent que text, donc word reçoit sa propre durée de vie (élidée). Écrire word: &'a str ferait compiler la fonction mais rejetterait cet appel avec E0597 : le compilateur supposerait alors que les résultats pourraient pointer dans query.
- Cette version du tokenizer compile, mais
mainnon. Expliquez l’erreur et corrigez-la en modifiant une seule ligne.
impl<'a> Parser<'a> { fn next_token(&mut self) -> Option<&str> { let start = self.pos; self.pos = self.input.len(); Some(&self.input[start..]) }}
let line = String::from("let x = 42");let mut parser = Parser { input: &line, pos: 0 };let first = parser.next_token();let second = parser.next_token();println!("{first:?} {second:?}");error[E0499]: cannot borrow `parser` as mutable more than once at a time --> e09_elided_self.rs:18:18 |17 | let first = parser.next_token(); | ------ first mutable borrow occurs here18 | let second = parser.next_token(); | ^^^^^^ second mutable borrow occurs here19 | println!("{first:?} {second:?}"); | ----- first borrow later used hereSolution
Avec Option<&str>, la règle d’élision 3 donne au résultat la durée de vie de &mut self. Tant que first est vivant, parser reste emprunté mutablement, donc le second appel est rejeté. Le token pointe en réalité dans l’entrée : dites-le.
fn next_token(&mut self) -> Option<&'a str> {Désormais, les tokens empruntent line, et le parser redevient libre dès que chaque appel se termine.