Durées de vie#

Le système de possession et d’emprunt garantit qu’aucune référence ne survit aux données qu’elle désigne. Pour les cas simples, le compilateur déduit automatiquement ces contraintes. Lorsque plusieurs références interagissent — dans une signature de fonction, une structure, ou un type générique — il devient nécessaire d’expliciter les relations entre leurs portées respectives au moyen d”annotations de durées de vie.

Annotations de durées de vie#

Définition 38 (Durée de vie (lifetime))

Une durée de vie est une région du code durant laquelle une référence est valide. Une annotation de durée de vie, notée 'a, ne modifie pas la durée de vie réelle d’une référence : elle exprime une contrainte indiquant au compilateur que plusieurs références doivent rester valides simultanément pendant au moins la durée 'a.

La syntaxe utilise une apostrophe suivie d’un identifiant, généralement court et en minuscules : 'a, 'b, 'src, etc. Les annotations apparaissent après le & dans les types de référence.

{
// Sans annotation (le compilateur infère la durée de vie)
fn longueur(s: &str) -> usize {
    s.len()
}

// Avec annotation explicite (ici facultative, mais illustrative)
fn longueur_annotee<'a>(s: &'a str) -> usize {
    s.len()
}

let mot = String::from("Rust");
println!("longueur = {}", longueur(&mot));
println!("longueur = {}", longueur_annotee(&mot));
}
longueur = 4
longueur = 4
()

Remarque 26

Les annotations de durées de vie ne prolongent ni ne réduisent la portée d’une variable. Elles servent uniquement à établir des relations entre les durées de vie de plusieurs références, afin que le vérificateur d’emprunts (borrow checker) puisse valider le programme.

Élision des durées de vie#

Dans la majorité des cas, le compilateur déduit les durées de vie sans qu’il soit nécessaire de les écrire. Ce mécanisme s’appelle l”élision des durées de vie (lifetime elision). Il repose sur trois règles appliquées séquentiellement aux signatures de fonctions.

Proposition 6 (Règles d’élision des durées de vie)

Le compilateur applique les règles suivantes pour inférer les durées de vie dans les signatures de fonctions :

  1. Règle des paramètres : chaque paramètre qui est une référence reçoit sa propre durée de vie distincte. Une fonction fn f(x: &str, y: &str) est traitée comme fn f<'a, 'b>(x: &'a str, y: &'b str).

  2. Règle du paramètre unique : s’il n’y a qu’un seul paramètre référence en entrée, sa durée de vie est assignée à toutes les références en sortie. Ainsi fn f(x: &str) -> &str devient fn f<'a>(x: &'a str) -> &'a str.

  3. Règle de self : si l’un des paramètres est &self ou &mut self, sa durée de vie est assignée à toutes les références en sortie, quel que soit le nombre de paramètres.

Exemple 36 (Application des règles d’élision)

Voir le code ci-dessous.

{
// Règle 2 : un seul paramètre référence → durée de vie propagée au retour
fn premier_mot(s: &str) -> &str {
    let octets = s.as_bytes();
    for (i, &octet) in octets.iter().enumerate() {
        if octet == b' ' {
            return &s[..i];
        }
    }
    s
}

let phrase = String::from("bonjour le monde");
let mot = premier_mot(&phrase);
println!("Premier mot : {}", mot);
}
Premier mot : bonjour
()

Lorsque les règles d’élision ne suffisent pas à déterminer toutes les durées de vie de sortie, le compilateur émet une erreur et exige des annotations explicites.

Durées de vie dans les signatures de fonctions#

Quand une fonction accepte plusieurs références et en retourne une, le compilateur ne peut pas deviner laquelle est liée au retour. Il faut alors annoter explicitement.

Exemple 37 (Annotation obligatoire avec plusieurs paramètres)

Voir le code ci-dessous.

{
fn le_plus_long<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() {
        x
    } else {
        y
    }
}

let chaine1 = String::from("longue chaîne");
let resultat;
{
    let chaine2 = String::from("court");
    resultat = le_plus_long(chaine1.as_str(), chaine2.as_str());
    println!("Le plus long : {}", resultat);
}
}
Le plus long : longue chaîne
()

L’annotation 'a signifie ici que la référence retournée vivra au moins aussi longtemps que la plus courte des durées de vie de x et y. Le compilateur prend l’intersection des deux portées.

Remarque 27

La durée de vie d’une valeur de retour doit toujours être liée à celle d’un paramètre. Retourner une référence vers une variable locale serait une référence pendante (dangling reference), ce que Rust interdit statiquement.

On peut également n’associer qu’une partie des paramètres à la durée de vie du retour.

{
fn avec_annonce<'a>(x: &'a str, _annonce: &str) -> &'a str {
    println!("Annonce : {}", _annonce);
    x
}

let s = String::from("données");
let r = avec_annonce(&s, "traitement en cours");
println!("{}", r);
}
Annonce : traitement en cours
données
()

Ici, _annonce n’a aucune relation de durée de vie avec la valeur retournée. Seule la durée de vie de x est pertinente.

Durées de vie dans les structures#

Lorsqu’une structure contient des références, chaque référence doit porter une annotation de durée de vie. Cela garantit que la structure ne peut pas survivre aux données qu’elle emprunte.

Définition 39 (Structure avec durée de vie)

Une structure contenant une référence doit déclarer un paramètre de durée de vie. La notation struct S<'a> indique que toute instance de S est liée à la durée de vie 'a des données empruntées. L’instance ne peut pas vivre plus longtemps que les données référencées.

Exemple 38 (Structure contenant une référence)

Voir le code ci-dessous.

{
#[derive(Debug)]
struct Extrait<'a> {
    texte: &'a str,
}

impl<'a> Extrait<'a> {
    // Règle 3 de l'élision : &self propage sa durée de vie au retour
    fn contenu(&self) -> &str {
        self.texte
    }

    fn niveau(&self) -> usize {
        self.texte.len()
    }
}

let roman = String::from("Il faut imaginer Sisyphe heureux.");
let e = Extrait { texte: &roman };

println!("Extrait : {:?}", e);
println!("Contenu : {}", e.contenu());
println!("Niveau : {} caractères", e.niveau());
}
Extrait : Extrait { texte: "Il faut imaginer Sisyphe heureux." }
Contenu : Il faut imaginer Sisyphe heureux.
Niveau : 33 caractères
()

Remarque 28

Dans le bloc impl<'a> Extrait<'a>, le paramètre 'a est déclaré après impl puis utilisé après le nom de la structure. Les méthodes prenant &self bénéficient de la troisième règle d’élision : la durée de vie de &self est propagée au retour sans annotation supplémentaire.

La durée de vie 'static#

Définition 40 (Durée de vie statique)

La durée de vie 'static désigne une référence valide pour toute la durée du programme. Toutes les chaînes littérales possèdent cette durée de vie : leur contenu est inscrit directement dans le binaire exécutable.

{
// Les littéraux de chaîne ont une durée de vie 'static
let s: &'static str = "je vis éternellement";
println!("{}", s);
}
je vis éternellement
()

Remarque 29

Lorsqu’un message d’erreur du compilateur suggère d’utiliser 'static, il est rarement judicieux de l’ajouter sans réflexion. Souvent, le problème sous-jacent est une référence pendante ou une durée de vie mal exprimée. La durée de vie 'static ne devrait être utilisée que lorsque la donnée vit réellement pour toute la durée du programme.

// Exemple : constante globale avec durée de vie 'static
static VERSION: &str = "1.0.0";

fn afficher_version() {
    let v: &'static str = VERSION;
    println!("Version : {}", v);
}

afficher_version();
Version : 1.0.0

Bornes de durées de vie dans les paramètres génériques#

Les durées de vie peuvent se combiner avec les paramètres génériques de type. On les déclare entre chevrons, avant les paramètres de type.

Exemple 39 (Combinaison de durée de vie et type générique)

Voir le code ci-dessous.

{
use std::fmt::Display;

fn afficher_et_retourner<'a, T: Display>(x: &'a str, y: T) -> &'a str {
    println!("Affichage : {}", y);
    x
}

let texte = String::from("résultat");
let r = afficher_et_retourner(&texte, 42);
println!("{}", r);
}
Affichage : 42
résultat
()

On peut également imposer qu’un paramètre de type générique contienne des références d’une certaine durée de vie, grâce à la borne T: 'a.

Définition 41 (Borne de durée de vie sur un type)

La borne T: 'a signifie que toutes les références contenues dans T doivent vivre au moins aussi longtemps que 'a. La borne T: 'static impose que T ne contienne aucune référence non statique (ou qu’il ne contienne pas de référence du tout).

{
#[derive(Debug)]
struct Ref<'a, T: 'a> {
    valeur: &'a T,
}

let nombre = 42;
let r = Ref { valeur: &nombre };
println!("{:?}", r);
}
Ref { valeur: 42 }
()

Sous-typage de durées de vie#

Définition 42 (Sous-typage de durées de vie)

La notation 'a: 'b (lire « 'a outlives 'b ») exprime que la durée de vie 'a est au moins aussi longue que 'b. Cela signifie que toute référence valide pendant 'a est également valide pendant 'b. En termes de sous-typage, 'a est un sous-type de 'b lorsque 'a dure au moins aussi longtemps.

Ce mécanisme est utile lorsqu’une fonction ou une structure manipule des références ayant des durées de vie distinctes mais liées par une relation d’inclusion.

Exemple 40 (Relation de sous-typage entre durées de vie)

Voir le code ci-dessous.

{
fn extraire_prefixe<'a, 'b>(texte: &'a str, _contexte: &'b str) -> &'a str
where
    'a: 'b,
{
    &texte[..3]
}

let long = String::from("Bonjour");
let court = String::from("ctx");
let prefixe = extraire_prefixe(&long, &court);
println!("Préfixe : {}", prefixe);
}
Préfixe : Bon
()

La clause where 'a: 'b garantit au compilateur que texte survivra au moins aussi longtemps que _contexte. La référence retournée, liée à 'a, est donc valide dans tout contexte où 'b est valide.

Exemple 41 (Structure avec plusieurs durées de vie)

Voir le code ci-dessous.

{
#[derive(Debug)]
struct Analyseur<'src, 'cfg> {
    source: &'src str,
    config: &'cfg str,
}

impl<'src, 'cfg> Analyseur<'src, 'cfg> {
    fn source(&self) -> &'src str {
        self.source
    }

    fn config(&self) -> &'cfg str {
        self.config
    }
}

let code = String::from("fn main() {}");
let opts = String::from("--edition 2021");
let a = Analyseur { source: &code, config: &opts };

println!("Source : {}", a.source());
println!("Config : {}", a.config());
}
Source : fn main() {}
Config : --edition 2021
()

Ici, les deux champs empruntent des données ayant potentiellement des durées de vie indépendantes. Utiliser deux paramètres distincts ('src et 'cfg) offre une flexibilité maximale, car le compilateur n’a pas besoin d’unifier les deux portées en une seule.

Remarque 30

En pratique, la plupart des programmes Rust ne nécessitent que peu d’annotations de durées de vie grâce aux règles d’élision. Elles deviennent indispensables dans trois situations principales : les fonctions retournant une référence parmi plusieurs paramètres références, les structures contenant des références, et les implémentations de traits sur des types empruntant des données.