Weak et Cow en Rust
Sur la page Les pointeurs intelligents, vous avez découvert Box, Rc et RefCell. Cette page ajoute deux outils de plus, qui règlent deux problèmes bien réels. Weak<T> vous permet de pointer vers une valeur sans la garder en vie. Cow<T> permet à une fonction de renvoyer des données empruntées quand c’est possible, et de ne faire une copie que quand c’est nécessaire.
Voyez Rc comme une clé de maison : tant que quelqu’un a une clé, la maison reste. Weak ressemble plutôt à l’adresse notée sur un bout de papier. Elle vous permet de trouver la maison, mais si tout le monde rend sa clé et que la maison disparaît, l’adresse ne mène plus nulle part. Personne ne se retrouve à payer pour une maison qu’il a oubliée.
Le problème : les cycles de Rc ne sont jamais nettoyés
Section intitulée « Le problème : les cycles de Rc ne sont jamais nettoyés »Une valeur Rc est libérée quand son compteur fort (strong count) tombe à 0. Ça marche très bien, jusqu’au jour où deux valeurs se tiennent l’une l’autre avec un Rc. Chacune garde alors l’autre en vie, pour toujours.
Pour le voir, le type Person ci-dessous implémente Drop : il affiche un message quand il est nettoyé. Regardez bien quels messages apparaissent :
use std::cell::RefCell;use std::rc::Rc;
struct Person { name: String, friend: RefCell<Option<Rc<Person>>>,}
impl Drop for Person { fn drop(&mut self) { println!("libération de {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice pointe vers bob, et bob pointe en retour vers alice *alice.friend.borrow_mut() = Some(Rc::clone(&bob)); *bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("compteur fort d'alice : {}", Rc::strong_count(&alice)); println!("compteur fort de bob : {}", Rc::strong_count(&bob)); println!("fin de main");}Sortie :
compteur fort d'alice : 2compteur fort de bob : 2fin de mainRemarquez ce qui manque : il n’y a ni libération de alice, ni libération de bob. Quand main se termine, les variables alice et bob disparaissent, donc chaque compteur passe de 2 à 1. Mais il n’atteint jamais 0, car chaque personne est encore tenue par le champ friend de l’autre. Cette mémoire a fui (on parle de fuite de mémoire, memory leak).
Ne vous en voulez pas si ça vous surprend. Le compilateur ne peut pas le détecter, et ce n’est pas un plantage. C’est simplement de la mémoire qui ne revient jamais. Dans un programme qui tourne longtemps et crée des cycles sans arrêt, ça finit par s’accumuler.
Weak<T> : un pointeur qui ne garde rien en vie
Section intitulée « Weak<T> : un pointeur qui ne garde rien en vie »La solution est de rendre l’un des liens, ou les deux, faibles (weak). On obtient un Weak à partir d’un Rc avec Rc::downgrade. Un lien faible ne compte pas comme propriétaire, il n’empêche donc pas la valeur d’être libérée.
Comme la valeur a peut-être déjà disparu, vous ne pouvez pas utiliser un Weak directement. Vous appelez .upgrade(), qui vous donne une Option<Rc<T>> : Some si la valeur est encore en vie, None sinon.
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Person { name: String, friend: RefCell<Weak<Person>>,}
impl Drop for Person { fn drop(&mut self) { println!("libération de {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob); *bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() { println!("l'ami d'alice est {}", friend.name); }
println!("alice : fort = {}, faible = {}", Rc::strong_count(&alice), Rc::weak_count(&alice)); println!("fin de main");}Sortie :
l'ami d'alice est bobalice : fort = 1, faible = 1fin de mainlibération de boblibération de aliceCette fois, les deux lignes libération apparaissent. Alice a un propriétaire fort (la variable alice) et un lien faible (celui de Bob). Quand main se termine, le compteur fort atteint 0, et Rust la nettoie. Bob part en premier, car les variables sont libérées dans l’ordre inverse de leur création.
Quelques points à retenir :
Weak::new()crée un pointeur faible vide, qui ne pointe vers rien.upgrade()sur lui renvoie toujoursNone. C’est une valeur de départ bien pratique.Rc::strong_countcompte les propriétaires.Rc::weak_countcompte les liens faibles. Seul le compteur fort décide du moment où la valeur est libérée.- Le
Rcque vous rendupgrade()est un vrai propriétaire, temporaire. Quand il sort de la portée, le compteur fort redescend.
Une arborescence de dossiers : les enfants possèdent, les parents sont faibles
Section intitulée « Une arborescence de dossiers : les enfants possèdent, les parents sont faibles »Les arbres sont le terrain classique de Weak. Un dossier possède ses sous-dossiers, donc children contient des Rc. Un sous-dossier veut aussi connaître son parent, mais il ne doit pas le posséder. Si les deux sens utilisaient Rc, vous auriez de nouveau un cycle. parent est donc un Weak :
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Folder { name: String, parent: RefCell<Weak<Folder>>, children: RefCell<Vec<Rc<Folder>>>,}
fn new_folder(name: &str) -> Rc<Folder> { Rc::new(Folder { name: name.to_string(), parent: RefCell::new(Weak::new()), children: RefCell::new(Vec::new()), })}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child);}
fn full_path(folder: &Folder) -> String { match folder.parent.borrow().upgrade() { Some(parent) => format!("{}/{}", full_path(&parent), folder.name), None => folder.name.clone(), }}
fn main() { let home = new_folder("home"); let photos = new_folder("photos"); let trips = new_folder("voyages");
add_child(&home, Rc::clone(&photos)); add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() { Some(parent) => println!("photos se trouve dans {}", parent.name), None => println!("photos n'a pas de parent"), }
println!("chemin complet : {}", full_path(&trips)); println!("home : fort = {}, faible = {}", Rc::strong_count(&home), Rc::weak_count(&home)); println!("photos : fort = {}, faible = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));}Sortie :
photos se trouve dans homechemin complet : home/photos/voyageshome : fort = 1, faible = 1photos : fort = 2, faible = 1Lisez les compteurs lentement. photos a deux propriétaires forts : la variable photos et la liste children à l’intérieur de home. Il a un lien faible : le champ parent de trips (le dossier voyages). home n’a qu’un seul propriétaire fort, sa propre variable, car personne ne possède son parent. C’est exactement la forme que vous voulez : l’ownership (la possession) descend l’arbre, et les liens qui remontent sont faibles.
RefCell est là parce que vous modifiez parent et ajoutez des éléments à children alors que les dossiers sont déjà dans un Rc, et qu’un Rc seul ne donne qu’un accès partagé, en lecture seule.
Des listes faibles qui ne gardent rien en vie
Section intitulée « Des listes faibles qui ne gardent rien en vie »Weak est aussi parfait pour les listes du genre « garder un œil sur ces choses, sans s’y accrocher » : des écouteurs (listeners), des observateurs, ou un cache d’objets que quelqu’un d’autre possède. Ici, le score d’un jeu envoie des mises à jour à ceux qui écoutent encore. Quand un écouteur est libéré, son lien faible cesse simplement de fonctionner :
use std::rc::{Rc, Weak};
struct Listener { name: String,}
fn notify(listeners: &[Weak<Listener>], event: &str) { for weak in listeners { match weak.upgrade() { Some(listener) => println!("{} a reçu : {event}", listener.name), None => println!("(un écouteur a disparu, on passe)"), } }}
fn main() { let screen = Rc::new(Listener { name: String::from("écran") }); let logger = Rc::new(Listener { name: String::from("journal") });
let mut listeners: Vec<Weak<Listener>> = Vec::new(); listeners.push(Rc::downgrade(&screen)); listeners.push(Rc::downgrade(&logger));
notify(&listeners, "le score est de 10");
drop(logger); // le journal est fermé par son propriétaire
notify(&listeners, "le score est de 20");
// faire le ménage dans les liens qui ne pointent plus vers rien listeners.retain(|weak| weak.upgrade().is_some()); println!("écouteurs restants : {}", listeners.len());}Sortie :
écran a reçu : le score est de 10journal a reçu : le score est de 10écran a reçu : le score est de 20(un écouteur a disparu, on passe)écouteurs restants : 1La liste n’a jamais empêché le journal d’être libéré. Si elle avait contenu des Rc, le journal aurait continué à vivre dans la liste, en recevant des messages dont personne ne voulait. Avec Weak, c’est le propriétaire qui décide quand quelque chose disparaît, et tous les autres vérifient simplement avec upgrade().
Cow : emprunter quand on peut, copier quand il le faut
Section intitulée « Cow : emprunter quand on peut, copier quand il le faut »Passons à un autre problème. Imaginons que vous écriviez une fonction qui nettoie des noms d’utilisateur. La plupart des noms sont déjà corrects. Quelques-uns ont des espaces en trop ou des majuscules. Si votre fonction renvoie toujours une nouvelle String, vous copiez chaque nom, même ceux que vous n’avez pas modifiés.
Cow (clone on write, « cloner à l’écriture », dans std::borrow::Cow) règle ce problème. Un Cow<'_, str> est un enum à deux variantes :
Cow::Borrowed(&str): « voici le texte d’origine, intact ». Aucune allocation.Cow::Owned(String): « j’ai dû le modifier, alors voici une nouvelleString».
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper { Cow::Borrowed(name) // déjà propre : on rend l'original } else { Cow::Owned(trimmed.to_lowercase()) // à corriger : on construit une nouvelle String }}
fn main() { for input in ["alice", " Bob ", "CAROL", "dave"] { match normalize(input) { Cow::Borrowed(name) => println!("{input:?} -> {name:?} (emprunté, aucune copie)"), Cow::Owned(name) => println!("{input:?} -> {name:?} (possédé, nouvelle String)"), } }}Sortie :
"alice" -> "alice" (emprunté, aucune copie)" Bob " -> "bob" (possédé, nouvelle String)"CAROL" -> "carol" (possédé, nouvelle String)"dave" -> "dave" (emprunté, aucune copie)Deux des quatre noms n’ont rien coûté du tout. Dans un vrai programme où 99 % des entrées sont déjà propres, cela fait beaucoup de copies évitées. Le '_ dans Cow<'_, str> veut simplement dire « ceci peut emprunter à l’entrée ». Si les lifetimes (durées de vie) vous semblent encore floues, la page Les lifetimes en Rust les explique.
Utiliser un Cow comme une chaîne normale
Section intitulée « Utiliser un Cow comme une chaîne normale »En général, vous n’avez même pas besoin de faire un match sur un Cow. Il se déréférence (deref) en &str : vous pouvez donc appeler des méthodes de chaîne dessus et passer &name à n’importe quelle fonction qui prend un &str. Quand il vous faut vraiment une String à vous, appelez .into_owned(). Elle sort la String s’il y en a une, et ne copie que si le Cow était emprunté.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) { Cow::Borrowed(name) } else { Cow::Owned(trimmed.to_lowercase()) }}
fn greet(name: &str) { println!("Bienvenue, {name} !");}
fn main() { let name = normalize(" Eve ");
println!("longueur : {}", name.len()); // les méthodes de str marchent directement println!("commence par e : {}", name.starts_with('e')); greet(&name); // &Cow<str> devient &str
let saved: String = name.into_owned(); // c'est maintenant une simple String println!("{saved:?} enregistré dans la liste des utilisateurs");}Sortie :
longueur : 3commence par e : trueBienvenue, eve !"eve" enregistré dans la liste des utilisateursCow fonctionne aussi avec d’autres types, comme Cow<'_, [i32]> pour une slice qui aura peut-être besoin d’être modifiée. Mais pour du texte, c’est Cow<'_, str> que vous verrez le plus souvent.
Quoi utiliser, et quand
Section intitulée « Quoi utiliser, et quand »Voici un petit aide-mémoire de tous les types « pointeurs » que vous avez rencontrés jusqu’ici :
| Type | Ce qu’il vous apporte | À utiliser quand |
|---|---|---|
Box<T> |
Un seul propriétaire, valeur sur le tas | Types récursifs, ou une grosse valeur que vous voulez déplacer à peu de frais |
Rc<T> |
Plusieurs propriétaires, un seul thread | Plusieurs parties du programme partagent les mêmes données |
Arc<T> |
Plusieurs propriétaires, entre threads | Comme Rc, mais partagé entre threads (voir L’état partagé) |
Weak<T> |
Un lien qui ne possède pas | Liens vers le parent, listes d’observateurs, caches, casser des cycles de Rc |
RefCell<T> |
Modification à travers une référence partagée, vérifiée à l’exécution | Vous devez modifier quelque chose qui vit dans un Rc |
Cow<'_, T> |
Emprunté ou possédé, décidé à l’exécution | Une fonction qui renvoie le plus souvent son entrée telle quelle |
Dans le doute, commencez par l’ownership et le borrowing (l’emprunt) tout simples. N’ajoutez ces types que quand le compilateur, ou une fuite de mémoire, vous dit que vous en avez besoin.
Erreurs courantes
Section intitulée « Erreurs courantes »Faire un unwrap sur upgrade alors que la valeur a disparu
Section intitulée « Faire un unwrap sur upgrade alors que la valeur a disparu »upgrade() renvoie une Option, et ce n’est pas pour rien. Si vous faites .unwrap() par habitude, votre programme fera un panic dès que la valeur aura été libérée :
use std::rc::Rc;
fn main() { let session = Rc::new(String::from("user-42")); let link = Rc::downgrade(&session);
drop(session); // la session se termine
let current = link.upgrade().unwrap(); println!("toujours connecté en tant que {current}");}À l’exécution, vous obtenez :
thread 'main' (12345) panicked at src/main.rs:9:34:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceCe message de Rust signifie : « Option::unwrap() a été appelé sur une valeur None ». La solution est de traiter « elle a disparu » comme un cas normal, avec match ou if let :
match link.upgrade() { Some(current) => println!("toujours connecté en tant que {current}"), None => println!("la session est terminée"),}Utiliser Rc pour le lien qui remonte
Section intitulée « Utiliser Rc pour le lien qui remonte »Si un enfant tient un Rc<Parent> et que le parent tient un Rc<Child>, vous avez construit le cycle de la première section. Rien ne plante et rien ne vous prévient ; la mémoire ne revient simplement jamais. Une bonne règle : décidez qui possède qui, utilisez Rc dans ce sens-là, et Weak pour chaque lien qui va dans l’autre sens.
struct Folder { parent: RefCell<Rc<Folder>>, // faux : l'enfant garde le parent en vie children: RefCell<Vec<Rc<Folder>>>,}Remplacez le type de parent par RefCell<Weak<Folder>>, et le cycle disparaît.
Exercices
Section intitulée « Exercices »1. Des commentaires sur un article supprimé
Section intitulée « 1. Des commentaires sur un article supprimé »Créez un Post avec un title, gardé dans un Rc. Créez un Comment avec un text et un post: Weak<Post>. Écrivez une fonction describe(comment: &Comment) qui affiche "<texte>" sur <titre> si l’article existe encore, ou "<texte>" sur un article supprimé sinon. Appelez-la, libérez l’article, puis appelez-la à nouveau. Indice : utilisez match comment.post.upgrade().
Voir la solution
use std::rc::{Rc, Weak};
struct Post { title: String,}
struct Comment { text: String, post: Weak<Post>,}
fn describe(comment: &Comment) { match comment.post.upgrade() { Some(post) => println!("\"{}\" sur {}", comment.text, post.title), None => println!("\"{}\" sur un article supprimé", comment.text), }}
fn main() { let post = Rc::new(Post { title: String::from("Apprendre Rust") }); let comment = Comment { text: String::from("Super article !"), post: Rc::downgrade(&post) };
describe(&comment); drop(post); describe(&comment);}Sortie :
"Super article !" sur Apprendre Rust"Super article !" sur un article suppriméLe commentaire ne garde jamais l’article en vie. Dès que le seul Rc est libéré, upgrade() renvoie None, et le commentaire gère ce cas calmement au lieu de faire un panic.
2. Échapper du texte seulement quand il le faut
Section intitulée « 2. Échapper du texte seulement quand il le faut »Écrivez fn escape(text: &str) -> Cow<'_, str>, qui remplace chaque < par <. Si le texte ne contient pas de <, renvoyez-le emprunté. Testez-la sur "bonjour le monde" et "1 < 2", et affichez la variante obtenue. Indice : vérifiez d’abord avec text.contains('<'), puis utilisez text.replace('<', "<") pour le cas possédé.
Voir la solution
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } else { Cow::Borrowed(text) }}
fn main() { for text in ["bonjour le monde", "1 < 2"] { match escape(text) { Cow::Borrowed(s) => println!("{s} (emprunté)"), Cow::Owned(s) => println!("{s} (possédé)"), } }}Sortie :
bonjour le monde (emprunté)1 < 2 (possédé)Seul le texte qui contient vraiment < paie pour une nouvelle String. Tout le reste passe directement, intact.
Récapitulatif
Section intitulée « Récapitulatif »- Deux valeurs
Rcqui pointent l’une vers l’autre n’atteignent jamais un compteur fort de 0 : elles fuient, et leurDropne s’exécute jamais. Rc::downgradecrée unWeak<T>, qui ne garde pas la valeur en vie.upgrade()rend uneOption<Rc<T>>.- Dans les arbres, les enfants sont possédés avec
Rcet les liens vers le parent sont desWeak. La possession descend, les liens faibles remontent. - Les listes d’observateurs et les caches peuvent contenir des
Weak, pour ne jamais garder quelque chose en vie par accident. Cow<'_, str>renvoie le texte emprunté quand rien n’a changé, et uneStringpossédée seulement quand il a fallu le modifier. Il se déréférence en&str, et.into_owned()vous donne uneString.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Deux valeurs tiennent chacune un Rc vers l'autre. Que se passe-t-il à la fin de main ?
2. Que renvoie weak.upgrade() ?
3. Dans une arborescence de dossiers, quel lien doit être un Weak ?
4. Quand une fonction qui renvoie un Cow<'_, str> évite-t-elle de créer une nouvelle String ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Reference Cycles Can Leak Memory — The Rust Book (en anglais) : la présentation officielle des cycles de
Rc, et d’un arbre avec des liensWeakvers le parent. std::rc::Weak— documentation de la bibliothèque standard (en anglais) : toutes les méthodes deWeak, dontupgrade,strong_countetWeak::new.std::rc— documentation de la bibliothèque standard (en anglais) : comment les compteurs fort et faible fonctionnent ensemble, avec un exemple plus long de cycles cassés.std::borrow::Cow— documentation de la bibliothèque standard (en anglais) : l’enumCow,to_mut,into_owned, et des exemples au-delà des chaînes de caractères.