iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Rust · Intermédiaire

Rust

Apprendre Rust de zéro jusqu'à un vrai outil en ligne de commande : ownership, emprunts, enums, gestion d'erreurs, traits, génériques, durées de vie, itérateurs, tests, threads et async avec Tokio.

RustCargoédition 2024serdeclapTokio
01Introduction et installation02Variables, types, fonctions et contrôle de flux03Ownership et emprunts04Structs, enums et pattern matching05Collections et gestion d'erreurs06Traits et génériques07Durées de vie et pointeurs intelligents08Closures et itérateurs09Modules, crates et tests10Concurrence : threads, canaux et async11Projet : un gestionnaire de tâches en ligne de commande
Chapitre 7·35 min

Durées de vie et pointeurs intelligents

Une durée de vie (lifetime) est la portion du programme pendant laquelle une référence est valide. Le compilateur les calcule tout seul dans 95 % des cas. Le reste du temps, il vous demande d'expliciter le lien entre plusieurs références : c'est la syntaxe 'a qui effraie tant de débutants. La seconde moitié du chapitre présente les pointeurs intelligents, Box, Rc, RefCell et Arc, qui servent quand une seule propriétaire ne suffit pas.

Le problème que résolvent les durées de vie

Cette fonction renvoie la plus longue de deux chaînes :

fn plus_longue(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
error[E0106]: missing lifetime specifier
  |
  | fn plus_longue(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`

Le compilateur ne regarde pas le corps des fonctions pour vérifier les emprunts de l'appelant : il se fie à la signature. Or celle-ci ne dit pas si le résultat vient de a ou de b. Et c'est important : si b disparaît avant qu'on utilise le résultat, la référence serait invalide.

Annoter une durée de vie

fn plus_longue<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
 
fn main() {
    let s1 = String::from("une longue phrase");
    let resultat;
    {
        let s2 = String::from("courte");
        resultat = plus_longue(&s1, &s2);
        println!("{resultat}"); // OK : s2 est encore vivante
    }
    // println!("{resultat}"); // erreur : s2 n'existe plus
}

'a se lit « une certaine durée de vie a ». La signature dit : le résultat est valide tant que a et b le sont. Une annotation ne change rien à la durée de vie réelle des valeurs. Elle décrit une relation, que le compilateur vérifie à chaque appel.

Si le résultat ne dépend que du premier paramètre, on le dit, et l'appelant gagne en liberté :

fn premier_mot<'a>(texte: &'a str, _separateur: &str) -> &'a str {
    texte.split(' ').next().unwrap_or("")
}

Quand peut-on omettre les annotations ?

Le compilateur applique trois règles d'élision qui couvrent la plupart des fonctions :

  1. Chaque paramètre référence reçoit sa propre durée de vie.
  2. S'il n'y a qu'un paramètre référence, sa durée de vie est donnée au résultat.
  3. Pour une méthode avec &self ou &mut self, la durée de vie de self est donnée au résultat.

C'est pour ça que fn premier_mot(s: &str) -> &str du chapitre 3 n'avait besoin de rien : un seul paramètre, règle 2. Dans la pratique, on n'écrit 'a que pour les fonctions qui reçoivent plusieurs références et en renvoient une.

Structs qui contiennent des références

Une struct peut emprunter des données au lieu de les posséder. Il faut alors annoter :

struct Extrait<'a> {
    texte: &'a str,
}
 
impl<'a> Extrait<'a> {
    fn premiere_phrase(&self) -> &'a str {
        self.texte.split('.').next().unwrap_or("")
    }
}
 
fn main() {
    let roman = String::from("Il était une fois. La suite plus tard.");
    let e = Extrait { texte: &roman };
    println!("{}", e.premiere_phrase());
}

L'annotation garantit qu'un Extrait ne survit jamais au texte qu'il emprunte.

Un conseil de débutant : si une struct avec des durées de vie vous fait souffrir, faites-lui posséder ses données (String au lieu de &str). C'est un peu moins efficace et beaucoup plus simple. On optimisera quand un profil le justifiera.

'static

'static désigne une référence valide pendant toute l'exécution du programme. Les littéraux de chaîne sont des &'static str : ils sont gravés dans l'exécutable.

fn salutation() -> &'static str {
    "bonjour"
}

Quand le compilateur suggère d'ajouter 'static pour corriger une erreur, méfiance : c'est rarement la bonne réponse. Le problème est presque toujours une référence qu'on essaie de garder trop longtemps.

Box<T> : une valeur sur le tas

Box place une valeur sur le tas et en garde la propriété unique. Il sert surtout dans deux cas : les objets trait du chapitre 6 (Box<dyn Forme>), et les types récursifs, dont la taille serait sinon infinie.

#[derive(Debug)]
enum Arbre {
    Feuille(i32),
    Noeud(Box<Arbre>, Box<Arbre>),
}
 
fn somme(a: &Arbre) -> i32 {
    match a {
        Arbre::Feuille(v) => *v,
        Arbre::Noeud(g, d) => somme(g) + somme(d),
    }
}
 
fn main() {
    let arbre = Arbre::Noeud(
        Box::new(Arbre::Feuille(1)),
        Box::new(Arbre::Noeud(Box::new(Arbre::Feuille(2)), Box::new(Arbre::Feuille(3)))),
    );
    println!("{}", somme(&arbre)); // 6
}

Sans Box, un Arbre contiendrait un Arbre qui contient un Arbre… Le compilateur ne pourrait pas calculer sa taille. Un Box a toujours la taille d'un pointeur.

Rc<T> : plusieurs propriétaires

Parfois une donnée a vraiment plusieurs propriétaires. Une configuration partagée par plusieurs composants, un nœud de graphe pointé par plusieurs autres. Rc (reference counted) compte les propriétaires et libère la valeur quand le dernier disparaît.

use std::rc::Rc;
 
struct Config {
    nom_app: String,
}
 
struct Service {
    config: Rc<Config>,
}
 
fn main() {
    let config = Rc::new(Config { nom_app: "iducation".into() });
    let api = Service { config: Rc::clone(&config) };
    let worker = Service { config: Rc::clone(&config) };
 
    println!("{} propriétaires", Rc::strong_count(&config)); // 3
    println!("{} / {}", api.config.nom_app, worker.config.nom_app);
}

Rc::clone ne copie pas la configuration : il incrémente un compteur. Par convention, on écrit Rc::clone(&x) plutôt que x.clone() pour que le lecteur voie que c'est bon marché.

Rc ne donne qu'un accès en lecture. Et il ne fonctionne que dans un seul thread : pour plusieurs threads, on utilise Arc, au chapitre 10.

RefCell<T> : modifier à travers une référence partagée

La règle « plusieurs lecteurs ou un seul écrivain » est vérifiée à la compilation. RefCell la déplace à l'exécution : on peut emprunter en écriture à travers une référence partagée, et si la règle est violée, le programme panique.

use std::cell::RefCell;
use std::rc::Rc;
 
fn main() {
    let historique = Rc::new(RefCell::new(Vec::<String>::new()));
 
    let journal = Rc::clone(&historique);
    journal.borrow_mut().push("connexion".into());
    journal.borrow_mut().push("lecture".into());
 
    println!("{:?}", historique.borrow()); // ["connexion", "lecture"]
}

Rc<RefCell<T>> est la combinaison classique pour une donnée partagée et modifiable dans un seul thread. Si vous en mettez partout, c'est souvent le signe que la conception lutte contre l'ownership : demandez-vous si un seul propriétaire, qui prête aux autres, ne suffirait pas.

Résumé des pointeurs

TypePropriétairesModificationThreadsUsage
Box<T>UnOui, s'il est mutOuiTypes récursifs, dyn Trait
Rc<T>PlusieursNonNonPartage en lecture
RefCell<T>UnOui, vérifié à l'exécutionNonMutation intérieure
Arc<T>PlusieursNonOuiPartage entre threads
Mutex<T>UnOui, avec verrouOuiMutation entre threads

Exercice

Une liste de tâches partagée entre deux « vues » : chacune peut ajouter une tâche, et les deux voient le même contenu. Utilisez Rc<RefCell<Vec<String>>>.

use std::cell::RefCell;
use std::rc::Rc;
 
struct Vue {
    nom: String,
    taches: Rc<RefCell<Vec<String>>>,
}
 
impl Vue {
    fn ajouter(&self, tache: &str) {
        self.taches.borrow_mut().push(format!("{} ({})", tache, self.nom));
    }
}
 
fn main() {
    let taches = Rc::new(RefCell::new(Vec::new()));
    let mobile = Vue { nom: "mobile".into(), taches: Rc::clone(&taches) };
    let web = Vue { nom: "web".into(), taches: Rc::clone(&taches) };
 
    mobile.ajouter("acheter du pain");
    web.ajouter("répondre aux mails");
 
    for t in taches.borrow().iter() {
        println!("- {t}");
    }
}

Remarquez que ajouter prend &self et pas &mut self : c'est la mutation intérieure permise par RefCell.


Les durées de vie ne sont qu'une façon d'écrire dans la signature ce que le compilateur vérifie déjà : aucune référence ne survit à sa donnée. Les pointeurs intelligents couvrent les cas où un propriétaire unique ne suffit pas. Le chapitre suivant revient à du code plus léger, et plus élégant : les closures et les itérateurs.

Précédent
Traits et génériques
Suivant
Closures et itérateurs

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main