Les pointeurs intelligents en Rust
Les règles d’ownership (possession) et de borrowing (emprunt) couvrent la plupart des situations. Mais quelques cas bien réels ne rentrent pas dans le moule « exactement un propriétaire » : une valeur dont la taille n’est connue qu’à l’exécution, des données qui ont vraiment besoin de plusieurs propriétaires, ou une valeur que vous devez modifier à travers une référence partagée. Les smart pointers (pointeurs intelligents) sont la réponse de Rust à chacun de ces cas.
Box<T> : mettre une valeur sur le tas
Section intitulée « Box<T> : mettre une valeur sur le tas »Par défaut, les valeurs vivent sur la pile (stack), et le compilateur doit connaître leur taille à l’avance. Box<T> déplace une valeur sur le tas (heap) et vous donne un pointeur de taille fixe vers elle. C’est utile quand la taille d’un type ne peut pas être connue à la compilation, le plus souvent avec les types récursifs :
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * pour récupérer la valeur}Sortie :
5Un exemple classique où vous avez besoin de Box : un type qui se contient lui-même, comme un nœud de liste chaînée :
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("Première valeur : {value}"), List::Nil => println!("Vide"), }}Sortie :
Première valeur : 1Sans Box, Cons(i32, List) demanderait une place infinie : une List qui contient une List qui contient une List, et ainsi de suite à l’infini. Box<List>, lui, est un pointeur de taille fixe, donc le compilateur peut calculer la place nécessaire.
Rc<T> : plusieurs propriétaires
Section intitulée « Rc<T> : plusieurs propriétaires »L’ownership dit qu’une valeur a exactement un propriétaire. La plupart du temps, c’est exactement ce qu’il faut. Mais parfois, plusieurs parties de votre programme ont vraiment besoin de partager les mêmes données. Rc<T> (reference counted, « à comptage de références ») le permet, en tenant le compte du nombre de propriétaires qui existent à un instant donné :
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("données partagées")); let b = Rc::clone(&a); // pas une copie profonde : juste un propriétaire de plus let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("Propriétaires : {}", Rc::strong_count(&a));}Sortie :
données partagées données partagées données partagéesPropriétaires : 3Rc::clone ne copie pas les données elles-mêmes : il augmente simplement un compteur et vous rend une autre référence vers la même valeur. Les données ne sont libérées qu’une fois que tous les propriétaires sont sortis de leur portée.
RefCell<T> : modifier à travers une référence partagée
Section intitulée « RefCell<T> : modifier à travers une référence partagée »Normalement, les règles du borrowing sont vérifiées à la compilation : soit plusieurs références en lecture seule, soit une seule référence mutable, jamais les deux. RefCell<T> déplace cette même vérification à l’exécution. Cela vous permet de modifier une valeur même à travers ce qui ressemble à une référence partagée, sans mut :
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}Sortie :
6RefCell est le plus souvent associé à Rc. Rc<RefCell<T>> vous donne une valeur qui a plusieurs propriétaires et qui peut quand même être modifiée, ce que Rc<T> seul ne permet pas.
Quand s’en servir vraiment
Section intitulée « Quand s’en servir vraiment »Ne commencez pas par les smart pointers. Commencez avec l’ownership et le borrowing classiques, et ne sortez ces outils que lorsque le compilateur vous dit clairement que ça ne rentre pas : un type récursif (Box), une valeur qui a vraiment besoin de plusieurs propriétaires (Rc), ou une modification à travers une référence partagée que les règles d’emprunt n’autorisent pas (RefCell).
Exercices
Section intitulée « Exercices »1. Additionner une liste chaînée
Section intitulée « 1. Additionner une liste chaînée »Reprenez l’enum List de la partie sur Box. Construisez la liste 1, 2, 3 et écrivez une fonction fn sum(list: &List) -> i32 qui additionne toutes les valeurs. Indice : faites un match sur la liste. Pour Cons(value, rest), la réponse est value plus la somme de rest. Pour Nil, c’est 0.
Voir la solution
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("Somme : {}", sum(&list));}Sortie :
Somme : 6La fonction s’appelle elle-même sur le reste de la liste, jusqu’à atteindre Nil. Remarquez que vous pouvez passer rest (un &Box<List>) là où un &List est attendu : Rust regarde à travers la Box pour vous.
2. Une liste de courses partagée
Section intitulée « 2. Une liste de courses partagée »Créez une seule liste de courses (un Vec<String>) que deux personnes, alice et bob, possèdent toutes les deux et peuvent toutes les deux compléter. Chacune ajoute un article. Affichez ensuite la liste et le nombre de propriétaires. Indice : enveloppez le Vec dans un Rc<RefCell<...>>, donnez à chaque personne un Rc::clone, et appelez .borrow_mut().push(...) pour ajouter un article.
Voir la solution
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("lait")); bob.borrow_mut().push(String::from("pain"));
println!("{:?}", list.borrow()); println!("Propriétaires : {}", Rc::strong_count(&list));}Sortie :
["lait", "pain"]Propriétaires : 3Il y a trois propriétaires : list, alice et bob. Tous les trois pointent vers le même Vec, donc un article ajouté par alice est aussi visible depuis bob et list. Aucune des variables n’a besoin de mut, car c’est RefCell qui gère la modification.
Récapitulatif
Section intitulée « Récapitulatif »Box<T>range une valeur sur le tas, derrière un pointeur de taille fixe : indispensable pour les types récursifs.Rc<T>permet d’avoir plusieurs propriétaires pour une même valeur dans du code à un seul thread, grâce au comptage de références ; entre plusieurs threads, utilisezArc<T>.RefCell<T>vérifie les règles d’emprunt à l’exécution plutôt qu’à la compilation, ce qui permet de modifier une valeur à travers une référence partagée, mais provoque une panic si les règles sont enfreintes.- Ne sortez ces outils que lorsque l’ownership et le borrowing classiques ne conviennent pas à votre situation.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Pourquoi un type récursif, comme un nœud de liste chaînée, a-t-il besoin de Box<T> ?
2. Que fait réellement Rc::clone(&a) ?
3. Quand RefCell<T> vérifie-t-il les règles d'emprunt ?
4. Qu'apporte Rc<RefCell<T>> que Rc<T> seul n'offre pas ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Using
Box<T>to Point to Data on the Heap — The Rust Book (en anglais) : tout surBox, y compris l’exemple de liste « cons » utilisé sur cette page. Rc<T>, the Reference Counted Smart Pointer — The Rust Book (en anglais) : la possession partagée pas à pas, en regardant le compteur monter et descendre.RefCell<T>and the Interior Mutability Pattern — The Rust Book (en anglais) : quand et pourquoi déplacer la vérification des emprunts à l’exécution, avec un exemple réaliste.std::cell— documentation de la bibliothèque standard (en anglais) : expliqueCelletRefCell, et quand choisir l’un ou l’autre.- Rc — Rust by Example (en anglais) : un court exemple exécutable qui affiche le compteur de références à mesure que les propriétaires apparaissent et disparaissent.