Vol. 6 · № 03 · Un atelier RustA Rust workshop

Cell / RefCell & l'interior mutability Cell / RefCell & interior mutability

Muter à travers un & partagé, au risque d'un panic à l'exécution. Le réflexe paresseux Rc<RefCell> qui réintroduit les bugs que le Vol 1 avait éliminés — un Vec + indices aurait suffi. Mutate through a shared &, at the risk of a runtime panic. The lazy Rc<RefCell> reflex that reintroduces the bugs Vol 1 eliminated — a Vec + indices would have sufficed.

AudienceAudience
Dev maîtrisant possession, Box et Rc/Arc (Vol 1, Vol 6 №1–2) Dev fluent in ownership, Box and Rc/Arc (Vol 1, Vol 6 №1–2)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●● Smart pointersRefCellInterior mutability
SommaireContents ·
№ 01CadrageFraming3 min

Muter à travers un & partagé — en déplaçant la vérification à l'exécution.Mutate through a shared & — by moving the check to runtime.

Le numéro précédent s'est clos sur un manque : Rc partage la possession, mais en lecture seule. La règle du Vol 1 l'impose — plusieurs &T, ou un seul &mut T, jamais les deux — et plusieurs Rc sont autant d'accès partagés. Or on a parfois besoin de muter une donnée partagée : un cache, un compteur, un nœud de graphe. La mutabilité intérieure (interior mutability) est la réponse de Rust, et elle repose sur un renversement. Elle ne supprime pas la règle « aliasing XOR mutation » — elle la maintient, mais déplace sa vérification de la compilation vers l'exécution. Cell et RefCell t'autorisent à muter derrière un &, et garantissent la sûreté en surveillant les emprunts au runtime. Le prix : ce qui était une erreur de compilation devient, en cas de faute, un panic.The previous issue closed on a gap: Rc shares ownership, but read-only. Vol 1's rule requires it — several &T, or a single &mut T, never both — and several Rcs are that many shared accesses. Yet you sometimes need to mutate shared data: a cache, a counter, a graph node. Interior mutability is Rust's answer, and it rests on a reversal. It doesn't remove the 'aliasing XOR mutation' rule — it keeps it, but moves its check from compile time to runtime. Cell and RefCell let you mutate behind a &, and guarantee safety by watching borrows at runtime. The price: what was a compile error becomes, on a fault, a panic.

RefCell — la mutation derrière un accès partagéRefCell — mutation behind a shared access
use std::cell::RefCell;

let compteur = RefCell::new(0);          // la donnée vit derrière une RefCell
let lecture = &compteur;                 // un accès PARTAGÉ (&), pas &mut

*lecture.borrow_mut() += 1;              // …et pourtant on mute À TRAVERS lui
println!("{}", lecture.borrow());        // 1

// la règle « un seul écrivain » tient toujours — mais elle est vérifiée
// à l'EXÉCUTION, pas à la compilation. l'enfreindre panique, au lieu de ne pas compiler.
La même règle, déplacée dans le tempsThe same rule, moved in time
&mut T
règle vérifiéerule checkedà la compilationat compile time
si on l’enfreintif brokenne compile pasdoesn't compile
RefCell<T>
règle vérifiéerule checkedà l’exécutionat runtime
si on l’enfreintif brokenpaniquepanics

C'est le geste de tout le volume, poussé à son terme. Le borrow checker du Vol 1 vérifie « aliasing XOR mutation » à la compilation : si tu enfreins la règle, ça ne compile pas. RefCell applique exactement la même règle, mais la vérifie à l'exécution : si tu l'enfreins, ça compile, ça tourne, et ça panique au moment précis de la double emprunte. Tu n'as pas gagné le droit de violer la règle — tu as seulement déplacé le moment où on te l'oppose. C'est un échange : plus de souplesse à l'écriture, moins de garanties statiques.It's the whole volume's move, taken to its end. Vol 1's borrow checker verifies 'aliasing XOR mutation' at compile time: break the rule, it won't compile. RefCell applies exactly the same rule, but checks it at runtime: break it, it compiles, it runs, and it panics at the precise moment of the double borrow. You haven't won the right to violate the rule — you've only moved when it's held against you. It's a trade: more flexibility when writing, fewer static guarantees.

Le réflexe du numéroThe issue's reflex

« Ai-je vraiment besoin de muter derrière un accès partagé — ou puis-je restructurer pour garder un &mut ? » La mutabilité intérieure est un outil de dernier recours, pas un confort. Tant qu'un &mut classique est atteignable (en réorganisant qui possède quoi), il est préférable : sa sûreté est prouvée à la compilation, gratuitement. RefCell n'entre que lorsque la structure rend le &mut statique réellement impossible.'Do I truly need to mutate behind a shared access — or can I restructure to keep a &mut?' Interior mutability is a last-resort tool, not a convenience. As long as a classic &mut is reachable (by reorganizing who owns what), it's preferable: its safety is proven at compile time, for free. RefCell enters only when the structure makes the static &mut genuinely impossible.

Cell ou RefCell ?Cell or RefCell?

Deux outils pour deux situations. Cell ne prête jamais de référence : on y met une valeur (set) ou on en sort une copie (get), en bloc — idéal pour des types Copy simples (un i32, un bool), sans aucun coût de suivi. RefCell, lui, prête de vraies références (borrow, borrow_mut) et doit donc surveiller les emprunts au runtime — plus puissant, mais avec le risque de panic. Copy et accès atomique → Cell ; besoin d'emprunter la donnée en place → RefCell.Two tools for two situations. Cell never lends a reference: you put a value in (set) or take a copy out (get), wholesale — ideal for simple Copy types (an i32, a bool), with no tracking cost. RefCell lends real references (borrow, borrow_mut) and so must watch borrows at runtime — more powerful, but with the panic risk. Copy and wholesale access → Cell; need to borrow the data in place → RefCell.

№ 02La surpriseThe surprise4 min

Une méthode &self qui modifie pourtant son objet.A &self method that modifies its object anyway.

Le Vol 1 a gravé une équivalence : &self veut dire lecture seule, &mut self veut dire mutation. Tenter d'écrire dans un champ depuis une méthode &self est un E0594 sans appel. C'est précisément ce qui bloque dès qu'une donnée est partagée — par Rc, ou simplement prêtée à plusieurs endroits : on n'a que des &, donc pas le droit de muter. La mutabilité intérieure brise cette équivalence d'une manière troublante : avec un champ RefCell, une méthode &self peut modifier l'objet. La mutabilité cesse d'être une propriété de l'accès (le & vs &mut) pour devenir une propriété de la donnée elle-même — le champ porte, en son type, le droit d'être muté en place.Vol 1 carved an equivalence: &self means read-only, &mut self means mutation. Trying to write to a field from a &self method is an unappealable E0594. That's exactly what blocks the moment data is shared — by Rc, or simply lent in several places: you only have &s, so no right to mutate. Interior mutability breaks this equivalence in an unsettling way: with a RefCell field, a &self method can modify the object. Mutability stops being a property of the access (& vs &mut) and becomes a property of the data itself — the field carries, in its type, the right to be mutated in place.

Le mur — &self est lecture seule, le compilateur refuseThe wall — &self is read-only, the compiler refuses
struct Compteur { valeur: i32 }

impl Compteur {
    fn incrementer(&self) {        // &self : accès PARTAGÉ, lecture seule
        self.valeur += 1;          // ✕ error[E0594]: cannot assign to `self.valeur`,
    }                              //   which is behind a `&` reference
}
// pour muter, il faudrait &mut self — mais tous les appelants n'y ont pas accès
// (une donnée partagée par Rc, par exemple, ne donne que des &).

L'équivalence du Vol 1 est sans ambiguïté : derrière un &, on lit, on ne mute pas. La solution évidente — passer à &mut self — n'est pas toujours disponible : si le Compteur est partagé par Rc, ou prêté à plusieurs lecteurs, aucun appelant ne détient de &mut. On est coincé entre « la donnée est partagée » et « je dois la modifier » — les deux que le Vol 1 déclarait incompatibles.Vol 1's equivalence is unambiguous: behind a &, you read, you don't mutate. The obvious fix — switch to &mut self — isn't always available: if the Compteur is shared by Rc, or lent to several readers, no caller holds a &mut. You're stuck between 'the data is shared' and 'I must change it' — the two Vol 1 declared incompatible.

La mutabilité intérieure déplace le droit de muter de l'accès vers la donnée : le type du champ, non la signature, décide.Interior mutability moves the right to mutate from the access to the data: the field's type, not the signature, decides.
№ 03La mécaniqueThe mechanics5 min

Un drapeau d'emprunt, suivi à l'exécution — et le panic qui sanctionne.A borrow flag, tracked at runtime — and the panic that sanctions.

RefCell tient, à côté de la donnée, un petit état d'emprunt : non emprunté, emprunté en partage par N lecteurs, ou emprunté en exclusivité par un écrivain. borrow() exige qu'aucun écrivain ne soit actif et incrémente le compteur de lecteurs ; borrow_mut() exige qu'aucun emprunt du tout ne soit actif et pose le verrou exclusif. Chaque garde retournée (Ref, RefMut) relâche son emprunt à sa destruction. C'est, trait pour trait, le borrow checker du Vol 1 — sauf qu'il s'exécute au runtime sur ce drapeau, au lieu d'être prouvé statiquement. Et là où le compilateur refusait de compiler, RefCell panique : la même règle, la même rigueur, un autre moment et une autre sanction.RefCell keeps, beside the data, a small borrow state: unborrowed, shared-borrowed by N readers, or exclusively borrowed by one writer. borrow() requires that no writer is active and increments the reader count; borrow_mut() requires that no borrow at all is active and sets the exclusive lock. Each returned guard (Ref, RefMut) releases its borrow when dropped. It is, trait for trait, Vol 1's borrow checker — except it runs at runtime on this flag, instead of being proven statically. And where the compiler refused to compile, RefCell panics: the same rule, the same rigor, another moment and another sanction.

Les états du drapeau d'emprunt — et la transition qui paniqueThe borrow flag's states — and the transition that panics
non empruntéunborrowed
—
borrow() →
⇄
← drop
partagé × Nshared × N
borrow()
borrow_mut() →
⇄
← drop
exclusif × 1exclusive × 1
borrow_mut()
✕2ᵉ emprunt incompatible → panic2nd incompatible borrow → panic
Le double emprunt qui paniqueThe double borrow that panics
let cell = RefCell::new(vec![1, 2, 3]);

let lecture = cell.borrow();          // emprunt partagé : drapeau = « lu par 1 »
let ecriture = cell.borrow_mut();     // ✕ déjà emprunté → panic à l'exécution :
//   thread 'main' panicked at src/main.rs:4:21:
//   RefCell already borrowed

// la règle « lecteurs XOR écrivain » est intacte — mais sa violation, qui était
// un refus de compilation, est devenue un panic. il faut LIBÉRER avant de muter :
drop(lecture);
let ecriture = cell.borrow_mut();     // ✓ plus aucun emprunt actif

L'erreur n'apparaît qu'à l'exécution, et seulement si le chemin fautif est emprunté. C'est tout le danger : un RefCell mal géré peut passer les tests heureux et paniquer en production, là où le borrow checker statique aurait garanti l'absence du problème avant même de tourner. La discipline est de garder les emprunts courts — ne pas tenir un borrow() plus longtemps que nécessaire — pour qu'aucun chevauchement involontaire ne survienne.The error appears only at runtime, and only if the faulty path is taken. That's the whole danger: a mishandled RefCell can pass the happy tests and panic in production, where the static borrow checker would have guaranteed the problem's absence before it even ran. The discipline is to keep borrows short — don't hold a borrow() longer than needed — so no accidental overlap occurs.

Cell : muter sans jamais prêterCell: mutate without ever lending
let cache = Cell::new(0u32);
cache.set(42);                  // remplace en bloc, aucune référence prêtée
let v = cache.get();            // sort une COPIE (Cell<T> exige souvent T: Copy)
let ancien = cache.replace(7);  // échange et rend l'ancien — toujours sans &

// Cell ne prête jamais : donc rien à surveiller, jamais de panic. Mais réservé
// aux valeurs qu'on remplace en entier, pas qu'on emprunte en place.
Pourquoi Cell ne panique jamais.Why Cell never panics.

Cell n'expose aucune référence vers son contenu : on ne peut que poser une valeur entière (set) ou en sortir une copie (get). Comme il n'y a jamais de référence vivante vers l'intérieur, il n'y a aucun emprunt à surveiller — donc aucun « RefCell already borrowed » possible. Le compromis : Cell convient aux types Copy ou aux remplacements en bloc, mais ne te laisse pas emprunter la donnée en place (pas de borrow_mut sur un gros Vec qu'on modifie élément par élément). Moins puissant que RefCell, mais sans risque de panic.Cell exposes no reference to its contents: you can only put a whole value in (set) or take a copy out (get). Since there's never a live reference to the inside, there's no borrow to watch — hence no possible 'RefCell already borrowed'. The trade-off: Cell suits Copy types or wholesale replacements, but won't let you borrow the data in place (no borrow_mut on a big Vec you edit element by element). Less powerful than RefCell, but with no panic risk.

RefCell est le borrow checker du Vol 1, rejoué à l'exécution : même règle, mais un panic là où il y avait une preuve.RefCell is Vol 1's borrow checker, replayed at runtime: same rule, but a panic where there was a proof.
№ 04Le piègeThe trap4 min

Le réflexe Rc<RefCell<T>> qui réintroduit les bugs du Vol 1.The Rc<RefCell<T>> reflex that brings back Vol 1's bugs.

Venant d'un langage à objets, on modélise volontiers un graphe ou un arbre par des nœuds qui se pointent mutuellement — et en Rust, la traduction littérale est Rc<RefCell<Noeud>>. Rc pour le partage, RefCell pour muter à travers. Ça compile, ça marche sur les petits cas. Mais on vient de rouvrir, d'un coup, toutes les portes que le Vol 1 avait fermées : les emprunts ne sont plus vérifiés statiquement (panic possible au runtime), et les cycles de Rc fuient (Vol 6 №2). On a reconstruit, en plus lourd, le modèle à pointeurs qu'on avait quitté. Or la plupart de ces structures ont une alternative qui garde les garanties statiques : l'arène — un Vec qui possède tous les nœuds, et des indices (usize) en guise de liens.Coming from an OO language, you readily model a graph or tree as nodes pointing at one another — and in Rust, the literal translation is Rc<RefCell<Noeud>>. Rc for sharing, RefCell to mutate through. It compiles, it works on small cases. But you've just reopened, at a stroke, every door Vol 1 closed: borrows are no longer checked statically (runtime panic possible), and Rc cycles leak (Vol 6 №2). You've rebuilt, more heavily, the pointer model you'd left behind. Yet most of these structures have an alternative that keeps the static guarantees: the arena — a Vec that owns all the nodes, and indices (usize) as links.

Le contre-exemple — le graphe tout-Rc<RefCell>, fragile et fuyantThe counter-example — the all-Rc<RefCell> graph, fragile and leaky
// Le réflexe : un graphe « à la Java », tout en Rc<RefCell<...>>
type Lien = Rc<RefCell<Noeud>>;
struct Noeud { valeur: i32, voisins: Vec<Lien> }

let a: Lien = Rc::new(RefCell::new(Noeud { valeur: 1, voisins: vec![] }));
let b: Lien = Rc::new(RefCell::new(Noeud { valeur: 2, voisins: vec![] }));
a.borrow_mut().voisins.push(Rc::clone(&b));
b.borrow_mut().voisins.push(Rc::clone(&a));   // cycle → fuite (Vol 6 №2)

// parcourir en mutant : si on tient un borrow_mut() de a et qu'on retombe
// sur a via un voisin → « RefCell already borrowed », panic à l'exécution. Et le cycle fuit.
a: Rc<RefCell>
⇄
b: Rc<RefCell>
↺ cycle = fuite · borrow_mut chevauché = panic↺ cycle = leak · overlapping borrow_mut = panic

Chaque nœud est une allocation séparée, reliée aux autres par des Rc qui peuvent former des cycles (fuite), et muté par des borrow_mut() qui peuvent se chevaucher (panic). Un parcours qui mute en suivant les voisins est un champ de mines : il suffit de retomber sur un nœud déjà emprunté. On a troqué les garanties de compilation du Vol 1 contre des risques d'exécution — sans même y gagner en simplicité.Each node is a separate allocation, linked to others by Rcs that can form cycles (leak), and mutated by borrow_mut()s that can overlap (panic). A traversal that mutates while following neighbors is a minefield: you need only land on an already-borrowed node. You've traded Vol 1's compile-time guarantees for runtime risks — without even gaining simplicity.

Le bon réflexe — l'arène : un Vec propriétaire, des indices pour liensThe right reflex — the arena: an owning Vec, indices as links
// L'arène : les nœuds dans un Vec, les liens sont des INDICES (usize)
struct Noeud { valeur: i32, voisins: Vec<usize> }
struct Graphe { noeuds: Vec<Noeud> }           // un seul propriétaire : le Vec

impl Graphe {
    fn ajouter(&mut self, v: i32) -> usize {    // &mut classique, vérifié à la compilation
        self.noeuds.push(Noeud { valeur: v, voisins: vec![] });
        self.noeuds.len() - 1                    // l'index sert d'« identité » du nœud
    }
}
// pas de Rc, pas de RefCell, pas de cycle, pas de panic. les indices se copient
// librement, et le borrow checker statique reprend tout son pouvoir.
Graphe.noeuds : Vec
[0]
Noeud
[1]
Noeud
[2]
Noeud
voisins: vec![0, 2] — de simples indices, jamais de cyclevoisins: vec![0, 2] — plain indices, never a cycle

Un seul propriétaire — le Vec — détient tous les nœuds ; les liens deviennent de simples usize, qu'on copie sans contrainte et qui ne forment jamais de cycle de possession. La mutation passe par un &mut self ordinaire sur le graphe, vérifié à la compilation : plus de panic, plus de fuite. Bonus : les nœuds contigus en mémoire sont plus rapides à parcourir. C'est l'écho du Vol 2, où l'on remplaçait une structure à références croisées par des indices — la même leçon, appliquée aux graphes.A single owner — the Vec — holds all the nodes; links become plain usize, copied freely and never forming an ownership cycle. Mutation goes through an ordinary &mut self on the graph, checked at compile time: no more panic, no more leak. Bonus: nodes contiguous in memory are faster to traverse. It echoes Vol 2, where a cross-referencing structure was replaced by indices — the same lesson, applied to graphs.

Quand Rc<RefCell> est vraiment justifiéWhen Rc<RefCell> is truly justified

Le verdict n'est pas « jamais » : Rc<RefCell> reste le bon outil quand le partage est réellement dynamique et imprévisible — un système d'observateurs, un nœud d'UI partagé dont la durée de vie échappe à toute hiérarchie. Mais ce sont les exceptions. Pour un graphe, un arbre, une liste — les cas où l'on dégaine Rc<RefCell> par habitude — l'arène est presque toujours plus simple, plus sûre et plus rapide.The verdict isn't 'never': Rc<RefCell> remains the right tool when sharing is genuinely dynamic and unpredictable — an observer system, a shared UI node whose lifetime escapes any hierarchy. But these are the exceptions. For a graph, a tree, a list — the cases where Rc<RefCell> is drawn by habit — the arena is almost always simpler, safer and faster.

Le test mentalThe mental test

« Y a-t-il un propriétaire naturel qui pourrait tenir tous ces objets dans un Vec ? » Presque toujours, pour un graphe ou un arbre, oui : le graphe lui-même. Si tu peux nommer ce propriétaire, l'arène s'impose, et tu récupères le borrow checker statique. Si la possession est vraiment éclatée et dynamique, sans propriétaire qui se dégage, alors — et seulement alors — Rc<RefCell> gagne son prix.'Is there a natural owner that could hold all these objects in a Vec?' Almost always, for a graph or tree, yes: the graph itself. If you can name that owner, the arena wins, and you regain the static borrow checker. If ownership is genuinely scattered and dynamic, with no owner emerging, then — and only then — does Rc<RefCell> earn its price.

Avant Rc<RefCell>, demande qui possède : un Vec et des indices rendent souvent au compilateur ce que la mutabilité intérieure lui retire.Before Rc<RefCell>, ask who owns: a Vec and indices often give the compiler back what interior mutability takes from it.
№ 05Bilan & éditoWrap-up & editorial2 min

Sortir de l'arbre a toujours le même prix : une garantie qui passe au runtime.Leaving the tree always has the same price: a guarantee moving to runtime.

Ce numéro clôt le Vol 6 sur le dernier déplacement. La mutabilité intérieure permet de muter derrière un accès partagé — non en abolissant la règle « aliasing XOR mutation », mais en déplaçant sa vérification de la compilation vers l'exécution. Cell remplace en bloc, sans jamais prêter, donc sans risque. RefCell prête de vraies références et surveille les emprunts au runtime — au prix d'un panic si on les chevauche. Et le piège ferme le volume : combiner Rc et RefCell par réflexe pour bâtir des graphes réintroduit les fuites et les panics que le Vol 1 avait éliminés, là où une arène — un Vec et des indices — aurait gardé toutes les garanties statiques.This issue closes Vol 6 on the final shift. Interior mutability lets you mutate behind a shared access — not by abolishing the 'aliasing XOR mutation' rule, but by moving its check from compile time to runtime. Cell replaces wholesale, never lending, hence riskless. RefCell lends real references and watches borrows at runtime — at the price of a panic if they overlap. And the trap closes the volume: combining Rc and RefCell by reflex to build graphs reintroduces the leaks and panics Vol 1 eliminated, where an arena — a Vec and indices — would have kept every static guarantee.

L'échappatoireThe escapeCe qu'elle déplace au runtimeWhat it moves to runtime
Box<T>Rien — un seul propriétaire ; juste une indirection sur le tas.Nothing — single owner; just a heap indirection.
Rc / ArcLe « qui libère » : décidé par un compteur, à l'exécution.The 'who frees': decided by a counter, at runtime.
RefCellLe borrow check : vérifié au runtime, panic si enfreint.The borrow check: verified at runtime, panic if broken.
CellRien à surveiller : remplace en bloc, jamais de référence prêtée.Nothing to watch: wholesale replace, no reference lent.
Chaque sortie de l'arbre échange une garantie statique contre une vérification dynamique — à ne payer que lorsque l'arbre ne suffit plus.Each exit from the tree trades a static guarantee for a dynamic check — to pay only when the tree no longer suffices.
Mot de l'éditeurFrom the editor

La même règle a gouverné six volumes. Le septième la lâche dans plusieurs threads.One rule has governed six volumes. The seventh sets it loose across several threads.

Regarde ce qui a tenu, d'un bout à l'autre. Le Vol 1 a posé « aliasing XOR mutation » : plusieurs lecteurs, ou un seul écrivain. Les Vol 2 à 5 l'ont déclinée — durées de vie, types, erreurs, traits. Le Vol 6 a montré comment l'assouplir quand l'arbre de possession ne suffit pas, toujours au prix d'un déplacement vers l'exécution. Une seule règle, six volumes de conséquences. Il reste un territoire où on la croit inapplicable, et où elle se révèle au contraire décisive : la concurrence.Look at what held, from end to end. Vol 1 set 'aliasing XOR mutation': several readers, or a single writer. Vols 2 to 5 declined it — lifetimes, types, errors, traits. Vol 6 showed how to relax it when the ownership tree falls short, always at the price of a shift to runtime. One rule, six volumes of consequences. One territory remains where it's thought inapplicable, and where it proves, on the contrary, decisive: concurrency.

Volume 7 : la concurrence sans peur. Le « fearless concurrency » de Rust n'est pas un slogan : c'est exactement la règle « aliasing XOR mutation » du Vol 1, appliquée aux threads. Un data race, c'est deux threads qui accèdent à la même donnée dont l'un écrit — soit précisément un aliasing avec mutation. Le borrow checker, qui interdisait déjà cela dans un seul thread, l'interdit entre threads : le data race ne s'attrape pas par un test, il ne compile pas. On verra les outils — thread::spawn et le move qui transfère la possession, les traits Send et Sync qui marquent ce qui peut traverser, Arc<Mutex> pour le partage mutable et sûr. La promesse du langage, et son prix, sur le terrain où les autres tremblent.Volume 7: fearless concurrency. Rust's 'fearless concurrency' isn't a slogan: it's exactly Vol 1's 'aliasing XOR mutation' rule, applied to threads. A data race is two threads accessing the same data with one writing — precisely aliasing with mutation. The borrow checker, which already forbade this within one thread, forbids it across threads: a data race isn't caught by a test, it doesn't compile. We'll see the tools — thread::spawn and the move that transfers ownership, the Send and Sync traits marking what may cross, Arc<Mutex> for safe shared mutation. The language's promise, and its price, on the ground where others tremble.