Lifetimes

Lifetimes

Les Lifetimes en Rust

Comprendre et Maîtriser les Durées de Vie

Le Concept le Plus Important de Rust

Objectifs du cours

À la fin de ce cours, tu sauras :

  • Comprendre ce qu'est une lifetime

  • Annoter les lifetimes correctement

  • Utiliser lifetimes dans les structs

  • Maîtriser lifetime elision

  • Résoudre les erreurs du borrow checker

1. Qu'est-ce qu'une lifetime ?

Une lifetime (durée de vie) est la portée pendant laquelle une référence est valide. C'est le mécanisme qui permet à Rust de garantir la sécurité mémoire sans garbage collector.

// Problème sans lifetimes (hypothétique) { let r; { let x = 5; r = &x; // x va être détruit ! } println!("{}", r); // I Dangling pointer ! } // Rust empêche ça avec les lifetimes fn main() { let r; { let x = 5; r = &x; // I Erreur de compilation ! // x does not live long enough } // println!("{}", r); } // Version correcte fn main() { let x = 5; let r = &x; println!("{}", r); // I OK }

I Lifetime = portée : Une référence ne peut jamais vivre plus longtemps que la donnée qu'elle référence. C'est le borrow checker qui vérifie ça !

2.1 - Syntaxe de base

// Annotation de lifetime avec '
// 'a se lit "lifetime a"
fn exemple<'a>(x: &'a str) -> &'a str {
    x
}
// Plusieurs paramètres
fn plus_long<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
// Convention de nommage
// 'a, 'b, 'c sont les plus courants
// Mais tu peux utiliser n'importe quel nom: 'lifetime, 'input, etc.

2.2 - Dans les fonctions

// Fonction qui retourne une référence
fn premier_mot<'a>(texte: &'a str) -> &'a str {
    let bytes = texte.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &texte[0..i];
        }
    }
    texte
}
// Utilisation
fn main() {
    let phrase = String::from("hello world");
    let mot = premier_mot(&phrase);
    println!("{}", mot);  // "hello"
}
// Pourquoi 'a ?
// Le compilateur sait que le retour vit aussi longtemps que l'input

2.3 - Plusieurs lifetimes

// Deux lifetimes différentes fn comparer<'a, 'b>(x: &'a str, y: &'b str) -> bool { x.len() > y.len() } // Le retour peut dépendre d'une seule fn choisir<'a, 'b>(x: &'a str, y: &'b str, premier: bool) -> &'a str { if premier { x // I OK, retourne 'a } else { // y // I ERREUR ! y a lifetime 'b, pas 'a x // Doit retourner x } } // Lifetime commune fn plus_long<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }

// Signifie: le retour vit aussi longtemps que
// la plus petite des deux lifetimes

II Règle d'or : Si une fonction retourne une référence, elle doit venir d'un des paramètres (ou être 'static).

3. Lifetime Elision

Le compilateur peut souvent inférer les lifetimes automatiquement grâce à des règles d'élision. C'est pourquoi tu n'as pas toujours besoin de les écrire !

3.1 - Règles d'élision

// Ces deux fonctions sont équivalentes:
// Sans annotation (élision)
fn premier(x: &str) -> &str {
    &x[0..1]
}
// Avec annotation explicite
fn premier<'a>(x: &'a str) -> &'a str {
    &x[0..1]
}
// Règles d'élision:
// 1. Chaque paramètre référence a sa propre lifetime
fn f(x: &str, y: &str)  // devient
fn f<'a, 'b>(x: &'a str, y: &'b str)
// 2. Si un seul param référence, sa lifetime = lifetime du retour
fn f(x: &str) -> &str  // devient
fn f<'a>(x: &'a str) -> &'a str
// 3. Si &self ou &mut self, sa lifetime = lifetime du retour
impl MonType {
    fn get_data(&self) -> &str  // devient
    fn get_data<'a>(&'a self) -> &'a str
}

3.2 - Quand annoter

// I PAS BESOIN d'annoter fn premiere_ligne(texte: &str) -> &str { texte.lines().next().unwrap_or("") } // I BESOIN d'annoter (plusieurs inputs) fn plus_long<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // I BESOIN d'annoter (struct avec référence) struct Extrait<'a> { contenu: &'a str, }

// I PAS BESOIN (méthode avec &self) impl<'a> Extrait<'a> { fn contenu(&self) -> &str { self.contenu // lifetime inférée de &self } }

I Conseil : Commence sans annotations. Si le compilateur se plaint, ajoute-les. Le message d'erreur te dira souvent quoi faire !

4.1 - Struct avec références

// Struct qui contient une référence
struct Article<'a> {
    titre: &'a str,
    contenu: &'a str,
}
impl<'a> Article<'a> {
    fn new(titre: &'a str, contenu: &'a str) -> Self {
        Article { titre, contenu }
    }
    fn apercu(&self) -> &str {
        &self.contenu[0..100.min(self.contenu.len())]
    }
}
// Utilisation
fn main() {
    let titre = String::from("Rust Lifetimes");
    let contenu = String::from("Les lifetimes sont...");
    let article = Article::new(&titre, &contenu);
    println!("{}", article.titre);
    // titre et contenu doivent vivre plus longtemps qu'article !
}

4.2 - Méthodes et lifetimes

struct Parser<'a> {
    texte: &'a str,
    position: usize,
}
impl<'a> Parser<'a> {
    fn new(texte: &'a str) -> Self {
        Parser { texte, position: 0 }
    }
    // Lifetime du retour = lifetime de &self
    fn reste(&self) -> &'a str {
        &self.texte[self.position..]
    }
    // Plusieurs lifetimes
    fn avec_prefixe<'b>(&'a self, prefixe: &'b str) -> String {
        format!("{}{}", prefixe, self.reste())
    }
}

5. 'static lifetime

'static est une lifetime spéciale qui dure pendant toute l'exécution du programme .

// Références 'static
let s: &'static str = "Hello world";
// Littéraux de chaîne sont toujours 'static
const MESSAGE: &'static str = "Constant";
// Owned data n'a pas besoin de 'static
let s = String::from("owned");  // Pas de lifetime !
// Trait bound 'static
fn process<T: 'static>(value: T) {
    // T doit être owned ou contenir seulement 'static refs
}

// Exemples valides process(42); // I i32 is 'static process(String::from("hello")); // I String is 'static process("literal"); // I &'static str

// Exemple invalide let s = String::from("temp"); // process(&s); // I &s n'est pas 'static

II 'static ne veut PAS dire immortel ! Un String peut être T: 'static mais être drop quand même. Ça veut juste dire "pas de références non-'static".

6. Patterns avancés

// 1. Lifetime bounds
struct Conteneur<'a, T: 'a> {
    item: &'a T,
}
// 2. Multiple struct lifetimes
struct Context<'s, 'c> {
    source: &'s str,
    config: &'c Config,
}
// 3. Lifetime in trait
trait Parser<'a> {
    fn parse(&self, input: &'a str) -> Result<&'a str, Error>;
}
// 4. Higher-rank trait bounds (HRTB)
fn apply<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str
{
    let s = String::from("hello");
    println!("{}", f(&s));
}

7. Résoudre les erreurs

Erreur : "does not live long enough"

// I Problème fn dangling_ref() -> &String { let s = String::from("hello"); &s // s est détruit ici ! } // I Solutions // 1. Retourner owned fn owned() -> String { String::from("hello") }

// 2. Utiliser 'static
fn static_ref() -> &'static str {
    "hello"
}
// 3. Prendre un paramètre
fn borrow_from_param(s: &String) -> &String {
    s
}

Erreur : "lifetime mismatch"

// I Problème fn choisir<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { if true { x } else { y } // y n'a pas lifetime 'a ! } // I Solution: même lifetime fn choisir<'a>(x: &'a str, y: &'a str) -> &'a str { if true { x } else { y } }

Exercice 1 : Annoter les lifetimes

// Ajoute les annotations nécessaires
fn plus_court(x: &str, y: &str) -> &str {
    if x.len() < y.len() { x } else { y }
}
// Solution :
fn plus_court<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() < y.len() { x } else { y }
}

Exercice 2 : Struct avec lifetime

// Crée une struct Citation qui contient:
// - une référence vers le texte
// - le nom de l'auteur (référence)
// Solution :
struct Citation<'a> {
    texte: &'a str,
    auteur: &'a str,
}
impl<'a> Citation<'a> {
    fn new(texte: &'a str, auteur: &'a str) -> Self {
        Citation { texte, auteur }
    }
    fn afficher(&self) {
        println!("'{}' - {}", self.texte, self.auteur);
    }
}

Aide-mémoire

SyntaxeSignification
&'a TRéférence avec lifetime 'a
fn f<'a>Fonction avec paramètre de lifetime
struct S<'a>Struct avec lifetime
impl<'a> S<'a>Implémentation pour S avec lifetime
T: 'aT contient seulement refs qui vivent au moins 'a
'staticLifetime de toute l'exécution
  • Lifetime = portée d'une référence
  • Borrow checker vérifie les lifetimes à la compilation
  • Élision permet d'éviter les annotations dans 90% des cas
  • 'a, 'b, 'c sont juste des noms de variables de lifetime
  • 'static = vit pendant toute l'exécution
  • Références ne peuvent jamais outlive leur donnée

Félicitations !

Tu as dompté les lifetimes !

C'est le concept le plus difficile de Rust, mais aussi le plus puissant. Maintenant tu comprends vraiment la magie du borrow checker !

I Tu es un vrai Rustacean ! I