backend / rust
Smart pointers : Box, Rc, RefCell
Explication
Ce que vous allez apprendre
- Utiliser
Box<T>pour des types récursifs ou de taille inconnue à la compilation - Partager la propriété d'une donnée entre plusieurs parties du code avec
Rc<T> - Comprendre la mutabilité intérieure de
RefCell<T>, vérifiée à l'exécution - Combiner
Rc<RefCell<T>>pour un état partagé et mutable mono-thread - Savoir pourquoi
Rcseul ne suffit jamais à muter une donnée partagée
Dans quel contexte ?
Un développeur veut modéliser un arbre binaire en Rust, où chaque nœud contient potentiellement deux sous-arbres. Il découvre rapidement que enum Arbre { Noeud(Arbre, Arbre) } ne compile jamais : le compilateur ne peut pas connaître la taille d'un Arbre à la compilation, puisqu'elle dépendrait d'elle-même de façon infinie. Box<Arbre> résout ce problème en ajoutant une indirection de taille fixe (un pointeur) entre chaque niveau.
D'abord, Box<T> est le plus simple des smart pointers
Box<T> alloue une valeur sur le heap plutôt que sur la pile, avec un ownership unique classique — exactement comme n'importe quelle autre valeur Rust, sauf que sa taille en mémoire (celle d'un pointeur) est toujours fixe, quelle que soit la taille réelle de la donnée qu'il contient. C'est ce qui rend possible les types récursifs comme un arbre ou une liste chaînée.
Une fois ce premier smart pointer maîtrisé, un besoin différent apparaît : le partage
Rc<T> (Reference Counted) permet à plusieurs parties du code de posséder simultanément la même donnée, en comptant combien de références actives existent. La donnée n'est libérée que lorsque le compteur retombe à zéro, c'est-à-dire quand plus aucune référence ne l'utilise.
| Smart pointer | Ownership | Mutabilité | Thread-safe |
|---|---|---|---|
Box<T> | Unique | Standard (via &mut) | Oui (si T l'est) |
Rc<T> | Partagé (compteur) | Non par défaut | Non, mono-thread uniquement |
RefCell<T> | Unique ou partagé (combiné) | Vérifiée à l'exécution | Non, mono-thread uniquement |
Prérequis
Il faut avoir bien assimilé l'ownership et le borrowing (leçons précédentes) : ces smart pointers existent précisément pour contourner, de façon contrôlée, certaines limitations strictes du borrow checker classique.
Il reste un problème non résolu par Rc seul : la mutation
Rc<T> donne un accès partagé, mais en lecture seule — le borrow checker interdit toujours plusieurs &mut simultanées, même à travers un Rc. RefCell<T> déplace cette vérification de la compilation vers l'exécution : .borrow_mut() panique immédiatement si un autre emprunt est déjà actif, plutôt que d'empêcher la compilation.
Piège dangereux
Appeler .borrow_mut() deux fois simultanément sur le même RefCell (par exemple en gardant le résultat du premier emprunt actif trop longtemps) provoque un panic à l'exécution, précisément le genre d'erreur que le borrow checker évite habituellement à la compilation. RefCell déplace le risque, il ne l'élimine pas : à utiliser uniquement quand le borrow checker statique est réellement trop restrictif pour le besoin.
Enfin, la combinaison la plus fréquente en pratique mono-thread
Rc<RefCell<T>> combine partage (Rc) et mutabilité intérieure (RefCell) : plusieurs parties du code peuvent posséder et modifier la même donnée, dans un contexte mono-thread. C'est un pattern extrêmement courant pour modéliser des structures de données comme des graphes ou des arbres avec des références croisées.
Bonne pratique
N'utilise Rc<RefCell<T>> qu'après avoir vérifié qu'une conception plus simple (ownership unique, références classiques) est réellement impossible pour ton cas : ce pattern déplace une partie de la sécurité du compilateur vers l'exécution, et doit rester une solution ciblée, pas un réflexe systématique.
Maintenant que tu maîtrises le partage mono-thread, la prochaine leçon passe au multi-thread avec Arc, l'équivalent thread-safe de Rc, et les mécanismes de synchronisation associés.
Commandes & code
Smart pointers : Box, Rc, RefCell
Au-delà des références simples, ces types gèrent des cas de propriété plus complexes.
use std::cell::RefCell;
use std::rc::Rc;
// Box<T> : allocation heap avec ownership unique, utile pour taille inconnue a la compilation
#[derive(Debug)]
enum Arbre {
Feuille(i32),
Noeud(Box<Arbre>, Box<Arbre>), // recursion : impossible sans indirection (taille infinie sinon)
}
fn somme_arbre(a: &Arbre) -> i32 {
match a {
Arbre::Feuille(v) => *v,
Arbre::Noeud(gauche, droite) => somme_arbre(gauche) + somme_arbre(droite),
}
}
// Rc<T> : Reference Counted, ownership PARTAGE (mono-thread uniquement)
struct Graphe {
noeuds: Vec<Rc<Noeud>>,
}
struct Noeud {
nom: String,
}
fn main() {
let arbre = Arbre::Noeud(
Box::new(Arbre::Noeud(Box::new(Arbre::Feuille(1)), Box::new(Arbre::Feuille(2)))),
Box::new(Arbre::Feuille(3)),
);
println!("{}", somme_arbre(&arbre));
// Rc : plusieurs proprietaires de la meme donnee, compteur de references
let a = Rc::new(Noeud { nom: String::from("A") });
println!("compteur apres creation: {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a); // n'incremente que le compteur, ne copie pas les donnees
println!("compteur apres clone: {}", Rc::strong_count(&a)); // 2
let graphe = Graphe { noeuds: vec![Rc::clone(&a), b] };
println!("compteur avec graphe: {}", Rc::strong_count(&a)); // 3
println!("{}", graphe.noeuds[0].nom);
// RefCell<T> : mutabilite interieure, verifiee a l'EXECUTION plutot qu'a la compilation
// Utile quand on doit muter une donnee partagee via Rc (Rc seul ne permet pas &mut)
let compteur_partage = Rc::new(RefCell::new(0));
let c1 = Rc::clone(&compteur_partage);
let c2 = Rc::clone(&compteur_partage);
*c1.borrow_mut() += 1; // borrow_mut() panique si un autre emprunt est deja actif
*c2.borrow_mut() += 10;
println!("valeur finale: {}", compteur_partage.borrow());
// Combinaison Rc<RefCell<T>> : pattern tres courant pour un etat partage mutable mono-thread
#[derive(Debug)]
struct CompteurPartage {
valeur: RefCell<i32>,
}
let cs = Rc::new(CompteurPartage { valeur: RefCell::new(0) });
let cs2 = Rc::clone(&cs);
*cs2.valeur.borrow_mut() += 5;
println!("{:?}", cs.valeur.borrow());
}Résumé
Box<T>: ownership unique sur le heap — indispensable pour les types récursifs ou de taille inconnue.Rc<T>: ownership partagé par comptage de références, mono-thread uniquement (utiliserArc<T>en multi-thread).RefCell<T>déplace la vérification d'emprunt de la compilation à l'exécution (panique si violée).Rc<RefCell<T>>est le pattern classique pour un état partagé et mutable dans un contexte mono-thread.
Exercices pratiques
Mission : un cache partage qui panique en production
Objectif : Diagnostiquer un panic de RefCell double-borrow et une struct recursive qui ne compile pas, puis les corriger.
Contexte
Un cache mono-thread partage entre plusieurs composants d'une appli utilise Rc<RefCell<HashMap<String, i32>>>. Un dev a ecrit une fonction qui, en cas de cle absente, lit la valeur par defaut puis l'insere dans le meme appel :
fn lire_ou_defaut(cache: &Rc<RefCell<HashMap<String, i32>>>, cle: &str) -> i32 {
let emprunt = cache.borrow_mut();
if let Some(v) = emprunt.get(cle) {
*v
} else {
drop(emprunt);
let mut emprunt2 = cache.borrow_mut();
emprunt2.insert(cle.to_string(), 0);
0
}
}Ce code fonctionne, mais un collegue a copie-colle un pattern similaire ailleurs sans le drop(emprunt), et l'application panique en production avec 'already borrowed: BorrowMutError'. Separement, l'equipe veut aussi modeliser une liste chainee simple et n'arrive pas a la faire compiler.