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

Annuler, sélectionner, structurer Cancel, select, structure

.await est un point d'annulation : select! garde le premier prêt et lâche les autres, drop annule une tâche. La cancellation safety qu'on oublie jusqu'à ce qu'elle morde. .await is a cancellation point: select! keeps the first ready and drops the rest, drop cancels a task. The cancellation safety you forget until it bites.

AudienceAudience
Dev connaissant Future/.await et le coloring (Vol 8 №1–2) Dev who knows Future/.await and coloring (Vol 8 №1–2)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●● AsyncselectCancellation
SommaireContents ·
№ 01CadrageFraming3 min

Le troisième visage du .await : un endroit où la tâche peut être interrompue.The third face of .await: a place where the task can be interrupted.

On a vu le .await sous deux angles : un point de suspension où l'exécuteur reprend la main (№1), un point de migration où la donnée tenue doit être Send (№2). En voici le troisième, et il clôt le volume : un point d'annulation. Un Future n'avance que tant qu'on le poll. Cesse de le poller — laisse-le tomber, drop-le — et il s'arrête à son dernier .await, définitivement : ce qu'il n'avait pas fini ne sera jamais fait. C'est radicalement différent des threads du Vol 7, qu'on ne pouvait pas interrompre proprement de l'extérieur. En async, l'annulation n'est pas un mécanisme spécial à câbler : elle est dans le modèle. Un timeout, une course entre deux requêtes, un serveur qu'on arrête — tout cela s'exprime en arrêtant de poller certains Futures. Annuler, c'est drop.We've seen .await from two angles: a suspension point where the executor takes back control (№1), a migration point where held data must be Send (№2). Here's the third, and it closes the volume: a cancellation point. A Future only advances while it's polled. Stop polling it — let it fall, drop it — and it stops at its last .await, for good: what it hadn't finished will never be done. This is radically different from Vol 7's threads, which you couldn't cleanly interrupt from outside. In async, cancellation isn't a special mechanism to wire up: it's in the model. A timeout, a race between two requests, a server you shut down — all of it is expressed by ceasing to poll certain Futures. To cancel is to drop.

Un Future n'avance que tant qu'on le poll — le lâcher l'arrêteA Future only advances while polled — dropping it stops it
poll
avanceadvances
▸
.await
suspendsuspends
▸
poll
reprendresumes
▸
.await
DROP iciDROP here
✕
???
ne tourne jamaisnever runs
Un timeout, c'est un drop déguiséA timeout is a disguised drop
use tokio::time::{timeout, Duration};

// timeout pilote `requete()` ; si 5 s passent, il LÂCHE le Future — qui s'arrête net.
match timeout(Duration::from_secs(5), requete()).await {
    Ok(reponse) => traiter(reponse),
    Err(_)      => println!("trop lent — la requête a été ANNULÉE"),
}
// annuler, ici, ce n'est PAS envoyer un signal : c'est cesser de poller le Future.
// il s'arrête à son dernier .await, et ce qu'il n'avait pas fini ne sera jamais fait.

timeout n'envoie aucun signal d'interruption à requete(). Il fait courir deux choses — la requête et un minuteur — et, dès que le minuteur gagne, il laisse tomber le Future de la requête. Celui-ci, n'étant plus pollé, s'arrête là où il en était. C'est tout. Cette simplicité est une force — l'annulation est gratuite et universelle — mais elle cache un piège : si la requête s'arrête au milieu d'une opération qu'elle croyait atomique, l'état qu'elle laisse peut être incohérent. C'est le sujet du chapitre 4.timeout sends no interrupt signal to requete(). It races two things — the request and a timer — and, the moment the timer wins, it drops the request's Future. That one, no longer polled, stops where it was. That's all. This simplicity is a strength — cancellation is free and universal — but it hides a trap: if the request stops in the middle of an operation it believed atomic, the state it leaves can be inconsistent. That's chapter 4's subject.

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

« Si cette tâche s'arrête net à ce .await, l'état qu'elle laisse est-il cohérent ? » Puisque tout .await est un point où la tâche peut être abandonnée, chacun est une question à se poser. Tant qu'une opération est atomique du point de vue de l'annulation — soit elle finit, soit elle n'a rien entamé d'irréversible — l'annulation est sans danger. Dès qu'elle laisse un travail à moitié fait derrière un .await, il faut y regarder de près.'If this task stops dead at this .await, is the state it leaves consistent?' Since every .await is a point where the task can be abandoned, each is a question to ask. As long as an operation is atomic from cancellation's standpoint — either it finishes, or it has started nothing irreversible — cancellation is harmless. The moment it leaves work half-done behind a .await, look closely.

Pourquoi les threads ne s'annulent pas ainsiWhy threads don't cancel like this

Au Vol 7, interrompre un thread de l'extérieur était quasi impossible proprement : aucune API portable pour le « tuer » sans laisser ses verrous et sa mémoire dans un état douteux. On en était réduit à un drapeau partagé que le thread devait vérifier lui-même. L'async renverse cela : comme un Future est une valeur qu'on drop, et que le drop déclenche le nettoyage (RAII, Vol 1), l'annulation devient une opération de premier ordre, sûre par défaut côté ressources. Le revers, c'est qu'elle peut survenir à n'importe quel .await, parfois là où on ne l'avait pas prévu.In Vol 7, interrupting a thread from outside was nearly impossible cleanly: no portable API to 'kill' it without leaving its locks and memory in a dubious state. You were reduced to a shared flag the thread had to check itself. Async flips this: since a Future is a value you drop, and dropping triggers cleanup (RAII, Vol 1), cancellation becomes a first-class operation, safe by default on the resource side. The flip side is that it can happen at any .await, sometimes where you didn't expect it.

№ 02La surpriseThe surprise4 min

select! garde le premier prêt — et abandonne les autres en plein vol.select! keeps the first ready — and abandons the others mid-flight.

select! est l'outil qui fait courir plusieurs Futures et garde le premier qui aboutit. Mais la moitié de son comportement est dans ce qu'il fait aux perdants : il les drop. Pour qui vient de JavaScript, c'est la surprise du numéro, et l'inverse exact de Promise.race. Là-bas, la course renvoie la première promesse résolue, mais les autres continuent à tourner en arrière-plan — la requête lente finit quand même, avec ses effets de bord et son coût, simplement on ignore son résultat. En Rust, les branches perdantes de select! sont annulées : leurs Futures sont lâchés à l'instant où une autre gagne, là où ils en étaient dans leur .await. Le travail en cours s'arrête. C'est plus propre — rien ne traîne — mais ça veut dire qu'une branche peut être interrompue au pire moment, et c'est exactement ce qui rend la cancellation safety nécessaire.select! is the tool that races several Futures and keeps the first to complete. But half its behaviour is in what it does to the losers: it drops them. For someone from JavaScript, that's the issue's surprise, and the exact inverse of Promise.race. There, the race returns the first resolved promise, but the others keep running in the background — the slow request finishes anyway, with its side effects and cost, you just ignore its result. In Rust, select!'s losing branches are cancelled: their Futures are dropped the instant another wins, wherever they were in their .await. The in-progress work stops. It's cleaner — nothing lingers — but it means a branch can be interrupted at the worst moment, and that's exactly what makes cancellation safety necessary.

select! — première branche prête, les autres lâchéesselect! — first branch ready, the others dropped
tokio::select! {
    reponse = serveur_a() => utiliser(reponse),   // si A gagne…
    reponse = serveur_b() => utiliser(reponse),   // …B est LÂCHÉ là où il en était (et inversement)
}
// select! garde la PREMIÈRE branche prête et DROP les autres, en plein .await.
// le perdant ne tourne pas en arrière-plan : il est annulé, point — sa requête s'arrête.

L'annulation des perdants découle directement du chapitre 1 : select! cesse de poller les branches qui n'ont pas gagné, donc leurs Future sont drop. Leurs ressources sont nettoyées (RAII), leurs requêtes interrompues. C'est idéal pour un timeout — « la réponse, ou deux secondes » — ou pour réagir au premier de plusieurs événements. Mais retiens ce que ça implique : une branche qui perd est arrêtée en plein milieu de ce qu'elle faisait. Si elle avait lu la moitié d'un message, cette moitié part avec elle.The cancellation of losers follows directly from chapter 1: select! stops polling the branches that didn't win, so their Futures are dropped. Their resources are cleaned up (RAII), their requests interrupted. It's ideal for a timeout — 'the response, or two seconds' — or to react to the first of several events. But remember what it implies: a losing branch is stopped in the very middle of what it was doing. If it had read half a message, that half goes with it.

En JS le perdant d'une course finit en silence ; en Rust il est annulé net — plus propre, mais interrompu au pire moment possible.In JS a race's loser finishes silently; in Rust it's cancelled dead — cleaner, but interrupted at the worst possible moment.
№ 03La mécaniqueThe mechanics5 min

join! attend tout le monde ; select! garde le premier — et Drop fait le ménage.join! awaits everyone; select! keeps the first — and Drop cleans up.

Deux combinateurs, deux intentions. join! prend plusieurs Futures, les fait avancer ensemble, et ne rend la main que lorsque tous ont fini — il rend un tuple de tous les résultats. select! prend plusieurs Futures, les fait avancer ensemble aussi, mais s'arrête au premier prêt, et lâche les autres. Point crucial, hérité du Vol 8 №1 : dans les deux cas, ces Futures tournent sur une seule tâche, entrelacés par l'exécuteur — c'est de la concurrence, pas du parallélisme. Pour du vrai parallélisme sur plusieurs threads, il faut tokio::spawn, qui détache une tâche indépendante. Et le mécanisme d'annulation est, lui, d'une simplicité élégante : annuler = drop, et drop déclenche le nettoyage via l'impl Drop (RAII, Vol 1). Fermer une connexion, relâcher un verrou, annuler une transaction : si c'est dans un Drop, l'annulation le fait pour toi.Two combinators, two intentions. join! takes several Futures, advances them together, and only returns when all have finished — it yields a tuple of all results. select! takes several Futures, advances them together too, but stops at the first ready, and drops the others. Crucial point, inherited from Vol 8 №1: in both cases, these Futures run on a single task, interleaved by the executor — it's concurrency, not parallelism. For true parallelism across threads, you need tokio::spawn, which detaches an independent task. And the cancellation mechanism is elegantly simple: cancel = drop, and drop triggers cleanup via the Drop impl (RAII, Vol 1). Closing a connection, releasing a lock, rolling back a transaction: if it's in a Drop, cancellation does it for you.

join! garde tout · select! garde le premier, lâche le restejoin! keeps all · select! keeps the first, drops the rest
join!
a
✓
b
✓
c
✓

toutes finissent → (a, b, c)all finish → (a, b, c)

select!
a
✓
b
✕
c
✕

le 1ᵉʳ prêt gagne · les autres → drop1st ready wins · the rest → drop

Les deux combinateurs, côte à côteThe two combinators, side by side
// join! : attend que TOUTES finissent, garde tout, rend un tuple
let (a, b, c) = tokio::join!(charger_a(), charger_b(), charger_c());

// select! : garde la PREMIÈRE prête, DROP les autres
let valeur = tokio::select! {
    v = travail()                       => v,
    _ = tokio::time::sleep(DEUX_S)      => defaut(),   // un timeout, exprimé comme une branche
};
// les deux font tourner plusieurs Futures sur UNE seule tâche : concurrence, pas parallélisme.

Note que le timeout du chapitre 1 n'est qu'un select! particulier : une branche « travail », une branche « minuteur », le premier prêt gagne. C'est la même primitive partout. Et rappelle-toi la frontière du №1 : join! et select! n'ajoutent aucun thread — ils entrelacent des Future sur la tâche courante. Ils servent à attendre plusieurs choses à la fois, pas à calculer plus vite. Pour cela, c'est spawn (tâche sur le runtime) ou les threads du Vol 7.Note that chapter 1's timeout is just a particular select!: a 'work' branch, a 'timer' branch, the first ready wins. It's the same primitive everywhere. And recall №1's boundary: join! and select! add no thread — they interleave Futures on the current task. They serve to await several things at once, not to compute faster. For that, it's spawn (a task on the runtime) or Vol 7's threads.

Annuler = drop, et drop déclenche le nettoyageCancel = drop, and drop triggers cleanup
struct Connexion { /* … */ }
impl Drop for Connexion {
    fn drop(&mut self) { self.fermer(); }   // le nettoyage est ICI (RAII, Vol 1)
}

async fn servir(c: Connexion) { boucle(c).await }
// si le Future de servir() est annulé (drop), `c` est drop → fermer() s'exécute.
// l'annulation déclenche le nettoyage des ressources, automatiquement. c'est ça,
// la « concurrence structurée » au niveau le plus simple : Drop fait le ménage.
spawn vs join!/select! : qui est annulé avec qui.spawn vs join!/select!: who's cancelled with whom.

Distinction qui sauve des heures. join! et select! gardent les Futures sur la tâche courante : si cette tâche est annulée, ils le sont tous avec elle — c'est structuré, lié à une portée. tokio::spawn, au contraire, détache une tâche indépendante qui vit sa vie sur le runtime : drop-er le JoinHandle ne l'annule pas (elle continue) ; pour l'arrêter, il faut appeler handle.abort(). Règle : ce qui vit dans un join!/select! meurt avec la portée ; ce qu'on spawn survit, sauf abort explicite.A distinction that saves hours. join! and select! keep the Futures on the current task: if that task is cancelled, they all are with it — it's structured, tied to a scope. tokio::spawn, by contrast, detaches an independent task that lives its own life on the runtime: dropping the JoinHandle doesn't cancel it (it keeps running); to stop it, you call handle.abort(). Rule: what lives in a join!/select! dies with the scope; what you spawn survives, barring explicit abort.

join! et select! entrelacent sur une tâche — concurrence, pas parallélisme ; et l'annulation, c'est Drop qui s'exécute.join! and select! interleave on one task — concurrency, not parallelism; and cancellation is Drop running.
№ 04Le piègeThe trap4 min

Une branche annulée au mauvais .await — et le travail à moitié fait disparaît.A branch cancelled at the wrong .await — and the half-done work vanishes.

Voici le piège qui donne son nom au numéro, et qui ne se révèle qu'en production. On a vu que select! annule les branches perdantes en plein .await. Tant que l'opération annulée n'avait rien entamé d'irréversible, aucun problème. Mais certaines opérations sont multi-étapes : elles lisent un bout, attendent, lisent la suite. Si l'annulation tombe entre deux étapes, le travail déjà fait — les octets lus, l'élément à moitié consommé — part avec le Future abandonné. C'est ce qu'on appelle la cancellation safety : une opération async est cancel-safe si, annulée à n'importe quel .await, elle ne perd aucun travail (soit elle a fini, soit elle n'a rien entamé). read_exact ne l'est pas ; recv() d'un canal l'est. Le danger est sournois parce qu'il n'apparaît jamais dans les tests heureux : il faut que l'annulation tombe pile au mauvais moment, sous charge, pour corrompre un flux.Here's the trap that names the issue, and that only surfaces in production. We've seen that select! cancels losing branches mid-.await. As long as the cancelled operation had started nothing irreversible, no problem. But some operations are multi-step: they read a chunk, wait, read more. If cancellation falls between two steps, the work already done — the bytes read, the half-consumed item — goes with the abandoned Future. This is called cancellation safety: an async operation is cancel-safe if, cancelled at any .await, it loses no work (either it finished, or it started nothing). read_exact isn't; a channel's recv() is. The danger is insidious because it never shows in the happy tests: cancellation must fall at exactly the wrong moment, under load, to corrupt a stream.

Une opération multi-étapes annulée à mi-chemin — le déjà-lu est perduA multi-step operation cancelled midway — what was read is lost
branche : socket.read_exact(&mut buf) — 12 octets attendusbranch: socket.read_exact(&mut buf) — 12 bytes expected
lit octets 1–4reads bytes 1–4
perdulost
▸
lit octets 5–8reads bytes 5–8
perdulost
▸
annulé : shutdown gagnecancelled: shutdown wins
▸
octets 9–12 jamais lusbytes 9–12 never read

↳ read_exact repart de zéro au tour suivant : les 8 octets déjà lus sont perdus → flux corrompu↳ read_exact restarts from zero next round: the 8 bytes already read are lost → corrupted stream

Le contre-exemple — read_exact dans un select! en boucleThe counter-example — read_exact in a select! loop
loop {
    tokio::select! {
        // ✕ read_exact n'est PAS cancel-safe : s'il est annulé en pleine lecture,
        //   les octets déjà placés dans `buf` sont PERDUS (l'appel repart de zéro).
        r = socket.read_exact(&mut buf) => traiter(&buf),
        _ = shutdown.recv()            => break,   // si shutdown gagne pendant un read partiel…
    }
}                                                  // …les octets à moitié lus disparaissent avec la branche.
Le bon réflexe — n'exposer au select! que du cancel-safeThe right reflex — expose only cancel-safe ops to select!
// le réflexe : dans un select! en boucle, n'utilise QUE des opérations cancel-safe
loop {
    tokio::select! {
        Some(msg) = canal.recv() => traiter(msg),  // ✓ recv() EST cancel-safe : rien à moitié lu
        _ = shutdown.recv()      => break,
    }
}
// et si une opération ne l'est pas (read_exact…) ? sors-la du select! : confie-la à une tâche
// dédiée, ou garde son état partiel TOI-MÊME entre deux tours — jamais dans la branche annulable.

La parade n'est pas d'éviter select! — c'est de ne lui confier que des opérations qui survivent à l'annulation. Le recv() d'un canal est cancel-safe : annulé, il n'a rien retiré du canal, le message est toujours là au tour suivant. Pour une opération qui ne l'est pas, deux sorties : la déplacer hors du select! (dans une tâche spawn qui la mène à terme à l'abri de l'annulation), ou posséder soi-même l'état partiel entre deux tours de boucle, de sorte qu'une annulation ne jette rien. La règle tient en une vérification : dans un select! répété, chaque branche doit pouvoir être coupée à son .await sans rien perdre.The fix isn't to avoid select! — it's to entrust it only with operations that survive cancellation. A channel's recv() is cancel-safe: cancelled, it removed nothing from the channel, the message is still there next round. For an operation that isn't, two exits: move it out of the select! (into a spawned task that drives it to completion, shielded from cancellation), or own the partial state yourself across loop rounds, so a cancellation discards nothing. The rule fits in one check: in a repeated select!, each branch must be able to be cut at its .await without losing anything.

Cancellation safety, la définitionCancellation safety, the definition

Une opération async est cancel-safe si l'annuler à un .await ne perd aucun travail : soit elle a abouti, soit elle n'a rien retiré ni modifié d'irréversible. C'est une propriété par opération, pas globale — et la doc de tokio l'indique méthode par méthode. Le réflexe : avant de mettre quelque chose dans une branche de select! répété, vérifie que c'est écrit « cancellation safe ». Si ça ne l'est pas, ne devine pas — restructure.An async operation is cancel-safe if cancelling it at a .await loses no work: either it completed, or it removed and modified nothing irreversible. It's a per-operation property, not global — and tokio's docs state it method by method. The reflex: before putting something in a repeated select! branch, check it's documented 'cancellation safe'. If it isn't, don't guess — restructure.

Pourquoi les tests ne l'attrapent pasWhy tests don't catch it

C'est le cousin async du data race : un bug de timing. Pour le déclencher, il faut qu'une annulation survienne exactement pendant qu'une branche est suspendue au milieu d'une opération multi-étapes — fenêtre étroite, qui ne s'ouvre que sous charge réelle, avec des latences réseau variables. Les tests unitaires, déterministes et rapides, ne la voient quasiment jamais. D'où la discipline préventive : on ne « teste » pas la cancellation safety après coup, on la garantit à l'écriture, en ne mettant que du cancel-safe dans les select! répétés.It's the async cousin of the data race: a timing bug. To trigger it, a cancellation must occur exactly while a branch is suspended in the middle of a multi-step operation — a narrow window, opening only under real load, with variable network latencies. Unit tests, deterministic and fast, almost never see it. Hence the preventive discipline: you don't 'test' cancellation safety after the fact, you guarantee it at writing time, by putting only cancel-safe operations in repeated select!s.

Dans un select! répété, chaque branche doit survivre à l'annulation : cancel-safe, ou hors du select!. Ne devine pas — lis la doc.In a repeated select!, each branch must survive cancellation: cancel-safe, or out of the select!. Don't guess — read the docs.
№ 05Bilan & éditoWrap-up & editorial2 min

Le .await avait trois visages : suspendre, migrer, annuler.The .await had three faces: suspend, migrate, cancel.

Ce numéro clôt le Vol 8 en révélant le dernier visage du .await. Un Future n'avance que tant qu'on le poll ; cesser de le poller — le drop — l'annule sur place. De là, tout découle : un timeout est un drop déclenché par un minuteur ; select! fait courir plusieurs Futures, garde le premier prêt et lâche les autres en plein vol, à l'inverse du Promise.race de JS où les perdants finissent en silence ; join! les attend tous. Ces combinateurs entrelacent sur une seule tâche — concurrence, pas parallélisme — et l'annulation, étant un drop, déclenche le nettoyage par RAII (Vol 1). Le piège ferme le volume : une opération multi-étapes annulée à mi-chemin perd son travail. La cancellation safety est la propriété qui dit si une branche peut être coupée sans dégât — à vérifier dans la doc, jamais à deviner.This issue closes Vol 8 by revealing the last face of .await. A Future only advances while polled; ceasing to poll it — dropping it — cancels it on the spot. From there, everything follows: a timeout is a drop triggered by a timer; select! races several Futures, keeps the first ready and drops the others mid-flight, unlike JS's Promise.race where losers finish silently; join! awaits them all. These combinators interleave on a single task — concurrency, not parallelism — and cancellation, being a drop, triggers cleanup via RAII (Vol 1). The trap closes the volume: a multi-step operation cancelled midway loses its work. Cancellation safety is the property that says whether a branch can be cut without damage — to check in the docs, never to guess.

L'outilThe toolCe qu'il faitWhat it does
drop d'un Futuredrop of a FutureL'annulation : la tâche s'arrête à son dernier .await ; Drop nettoie.Cancellation: the task stops at its last .await; Drop cleans up.
timeoutUn drop déclenché par un minuteur : la réponse, ou l'abandon.A drop triggered by a timer: the response, or the abandon.
select!Garde la 1ʳᵉ branche prête, lâche (annule) les autres.Keeps the 1st ready branch, drops (cancels) the others.
join!Attend que toutes finissent, rend un tuple. Sur une seule tâche.Awaits all, returns a tuple. On a single task.
cancellation safetycancellation safetyAnnulable sans perte ? À vérifier par opération dans la doc.Cancellable without loss? Check per operation in the docs.
Annuler, en async, c'est cesser de poller : gratuit et universel — à condition que chaque .await survive à l'interruption.To cancel, in async, is to stop polling: free and universal — provided each .await survives the interruption.
Mot de l'éditeurFrom the editor

Huit volumes à rendre des bugs impossibles. Le neuvième : connaître le coût.Eight volumes making bugs impossible. The ninth: knowing the cost.

Le Vol 8 a montré une concurrence d'un autre genre que le Vol 7 : non pour calculer en parallèle, mais pour attendre sans bloquer. Un Future est paresseux (№1), il colore la pile et réveille Send (№2), il s'annule par un simple drop (№3). Trois visages d'un même .await — suspendre, migrer, annuler — et une leçon constante : l'async est un outil précis, pour un besoin précis, avec un coût qu'il vaut mieux connaître avant de l'adopter. Or « connaître le coût » est, justement, le dernier réflexe qui manque à cette série. On a passé huit volumes à rendre des classes entières de bugs impossibles. Reste une question qu'on a tenue à distance : qu'est-ce que tout cela coûte, vraiment ?Vol 8 showed concurrency of a different kind than Vol 7: not to compute in parallel, but to wait without blocking. A Future is lazy (№1), it colors the stack and wakes Send (№2), it cancels by a simple drop (№3). Three faces of one .await — suspend, migrate, cancel — and a constant lesson: async is a precise tool, for a precise need, with a cost better known before adopting it. And 'knowing the cost' is, precisely, the last reflex this series lacks. We've spent eight volumes making whole classes of bugs impossible. One question remains, held at arm's length: what does all this actually cost?

Vol 9 : le coût réel & l'échappatoire — le dernier volume, et pas par hasard. Règle de la série : on n'y entre qu'avec un profil, jamais avec une intuition. On y verra que la chaîne .iter().map().filter().collect() compile vers la même boucle qu'à la main — l'abstraction zéro-coût, prouvée plutôt que promise ; que le .clone() posé « pour faire taire le compilateur » cache parfois une allocation qu'un emprunt ou un Cow aurait évitée ; et enfin unsafe, l'échappatoire au compilateur, qui ne désactive pas ses règles mais t'en transfère la preuve. On commence par le plus rassurant : pourquoi, en Rust, la lisibilité des itérateurs ne coûte rien.Vol 9: the real cost & the escape hatch — the last volume, and not by chance. The series' rule: you enter only with a profile, never with a hunch. We'll see that the chain .iter().map().filter().collect() compiles to the same loop as by hand — the zero-cost abstraction, proven rather than promised; that the .clone() dropped in 'to silence the compiler' sometimes hides an allocation a borrow or a Cow would have spared; and finally unsafe, the escape hatch from the compiler, which doesn't disable its rules but transfers their proof to you. We start with the most reassuring: why, in Rust, the readability of iterators costs nothing.