Acheter version papier

Rust

Fork me on GitHub

II. Spécificités de Rust

6. Durée de vie (ou lifetime)

Il existe deux types de durée de vie :

  • Les durées de vie statiques.
  • Les durées de vie temporaires.

Les durées de vie statiques

Les durées de vie statiques permettent d'avoir des références sur des variables statiques ou du contenu "constant" :

Run// Avec une variable statique :
static VAR: i32 = 0;
let variable_statique: &'static i32 = &VAR;

// Avec une constante :
const CONST_VAR: i32 = 0;
let variable_constante: &'static i32 = &CONST_VAR;

// Avec du contenu constant (car une string écrite "en dur" dans le code est
// stockée telle quelle dans le code compilé) :
let variable_const: &'static str = "Ceci est une str constante !";

Une durée de vie statique veut donc dire que le contenu qu'elle référence vivra du début à la fin du programme.

Les durées de vie temporaires

Les durées de vie temporaires sont un peu plus complexes mais aussi moins visibles la plupart du temps. Imaginons que l'on écrive une structure dont l'un des champs devait être modifié à l'extérieur de la structure. On se contenterait de renvoyer &mut self.ma_variable. Bien que ce code fonctionne, il est important de comprendre ce qu'il se passe :

Runstruct MaStruct {
    variable: String,
}

impl MaStruct {
    fn get_variable(&mut self) -> &mut String {
        &mut self.variable
    }
}

fn main() {
    let mut v = MaStruct { variable: String::new() };

    v.get_variable().push_str("hoho !");
    println!("{}", v.get_variable());
}

La méthode get_variable va en fait renvoyer une référence temporaire sur self.variable. Si on voulait écrire ce code de manière "complète", on l'écrirait comme ceci :

Runimpl MaStruct {
    fn get_variable<'a>(&'a mut self) -> &'a mut String {
        &mut self.variable
    }
}

'a représente la durée de vie (cela aurait tout aussi bien pu être 'x ou 'zaza, peu importe). Ici, on retourne donc une référence avec une durée de vie 'a sur une variable.

Ici le compilateur fait ce que l'on appelle de l'élision. Comme il n'y a qu'une seule durée de vie possible pour cette variable, il la déduit automatiquement donc pas besoin de l'ajouter nous-même. Cependant il y a beaucoup de cas où il ne peut pas, comme par-exemple :

Runfn foo(a: &str, b: &str) -> &str {
    a
}

fn main() {
    let c = foo("a", "b");
}

Ce code renvoie cette erreur :

1 | fn foo(a: &str, b: &str) -> &str {
  |           ----     ----     ^ expected named lifetime parameter

Dans le cas présent, il y a plusieurs durées de vie possibles et il ne sait pas laquelle choisir, il faut donc ajouter les durées de vie nous-même :

Runfn foo<'a, 'b>(a: &'a str, b: &'b str) -> &'a str {
    a
}

fn main() {
    let c = foo("a", "b");
}

Types avec une référence comme champ

Les itérateurs sont un exemple assez courant où un type contient un champ qui est une référence. Pour l'illustrer, on va écrire un itérateur sur une String qui renvoie chaque ligne non vide :

Runstruct LineIterator<'a> {
    content: &'a str,
}

// Comme le type `LineIterator` contient une durée de vie, il faut aussi la
// déclarer sur tous les impl blocks.
impl<'a> LineIterator<'a> {
    fn new(content: &'a str) -> LineIterator<'a> {
        LineIterator { content }
    }

    fn retourne_substring(
        &mut self,
        début: usize,
        dernier: usize,
    ) -> Option<&'a str> {
        if dernier <= début {
            // Si jamais la string est vide, cela signifie que l'on a atteint
            // la fin de notre string donc qu'il n'y a plus rien à retourner.
            return None;
        }
        // On récupère la sous-string que l'on va retourner.
        let ret = &self.content[début..dernier];
        // On change la position du début de notre string.
        self.content = &self.content[dernier..]; 
        Some(ret)
    }
}

// On implémente le trait `Iterator` par commodité.
impl<'a> Iterator for LineIterator<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<Self::Item> {
        let mut indices = self.content.char_indices();
        let mut début = 0;

        // D'abord on passe tous les retours à la ligne pour arriver au contenu.
        while let Some((pos, c)) = indices.next() {
            if c != '\n' {
                début = pos;
                break;
            }
        }
        while let Some((pos, c)) = indices.next() {
            if c == '\n' {
                // On a trouvé un retour à la ligne donc on renvoie ce qu'on a
                // trouvé.
                return self.retourne_substring(début, pos);
            }
        }
        // Nous avons atteint la fin de notre string, on renvoie tout le
        // contenu.
        self.retourne_substring(début, self.content.len() - 1)
    }
}

fn main() {
    // On crée notre itérateur.
    let iterator = LineIterator::new("a\n\nbc\n");
    // On récupére toutes les `String`s dans un vecteur.
    let strings = iterator.into_iter().collect::<Vec<_>>();
    // Si tout s'est bien passé, cet `assert_eq` ne devrait pas paniquer.
    assert_eq!(strings, vec!["a", "bc"]);
}

Il est bon de noter que nous aurions pu remplacer la durée de vie ('a) du champ content par 'static. Cependant, faire cela nous aurait empêcher d'utiliser autre chose que des str statiques, ce qui aurait été une grosse limitation.

Un autre cas d'usage assez répandu pour l'utilisation des références directement dans un type est pour les parseurs. Le plus souvent, vous n'avez pas besoin de prendre la propriété de la donnée que vous souhaitez parser. Cela offre le plus souvent la possibilité d'éviter des allocations qui ne sont pas nécessaires. Dans l'exemple que l'on vient de voir, il n'y a aucune allocation pour les str puisqu'on ne renvoie que des "vues" sur un espace mémoire. Si vous avez besoin de modifier ce contenu, vous pouvez toujours le faire de votre côté en allouant la mémoire nécessaire.

Contraintes sur les durées de vie

Tout comme on peut ajouter des contraintes sur les traits avec les supertraits, on peut aussi ajouter des contraintes sur les durées de vie :

Runfn foo<'a, 'b: 'a>(a: &'a str, b: &'b str) -> &'a str {
    a
}

Ici, on indique au compilateur que la durée de vie 'b doit vivre au moins aussi longtemps que 'a. Cela reste cependant une utilisation avancée des durées de vie et il y a peu de chances que vous en croisiez, mais il semblait important que vous soyiez au courant au cas où vous veniez à en rencontrer.

D'ailleurs, tout comme pour les arguments génériques, il est possible d'utiliser le mot-clé where pour améliorer la lisibilité des durées de vie :

Runfn foo<'a, 'b, 'c>(a: &'a str, b: &'b str, c: &'c str) -> &'c str
where
    'b: 'a,
    'c: 'b + 'c,
{
    c
}