Aller au contenu

Astuces de la bibliothèque standard en Rust : les petits outils du quotidien

Dans toute cuisine, il y a un tiroir rempli de petits ustensiles : un économe, un décapsuleur, un presse-ail. Vous pourriez tout faire avec un seul grand couteau, mais ces petits outils rendent le travail plus rapide et plus sûr. La bibliothèque standard de Rust a elle aussi un tiroir de ce genre.

Cette page vous fait visiter ce tiroir. Chaque astuce est courte : une image de la vie courante, un exemple à exécuter et une ligne « à utiliser quand… » pour savoir quand la sortir. Inutile de tout apprendre d’un coup. Parcourez la page maintenant, et revenez-y quand un bout de votre code vous semblera maladroit.

Sortir une valeur d’un &mut : mem::take, replace, swap

Section intitulée « Sortir une valeur d’un &mut : mem::take, replace, swap »

Imaginez une poubelle avec un sac dedans. Vous avez le droit de sortir le sac plein, mais seulement si vous en remettez un neuf tout de suite. La poubelle n’est jamais laissée sans sac.

Rust applique la même règle. Si vous n’avez que &mut self, vous ne pouvez pas simplement sortir un champ par move (déplacement), car cela laisserait un trou dans la struct. Le compilateur vous arrête avec E0507. Les fonctions de std::mem règlent le problème en laissant toujours quelque chose à la place :

  • mem::take(&mut x) prend la valeur et laisse la valeur par défaut du type (un Vec vide, une String vide, 0, None).
  • mem::replace(&mut x, new) met new à la place et vous rend l’ancienne valeur.
  • Option::take() prend ce qu’il y a dans une Option et laisse None.
use std::mem;
struct Inbox {
unread: Vec<String>,
status: String,
draft: Option<String>,
}
impl Inbox {
// Rend tous les messages non lus et laisse un Vec vide à la place.
fn read_all(&mut self) -> Vec<String> {
mem::take(&mut self.unread)
}
// Met un nouveau statut et rend l'ancien.
fn set_status(&mut self, new: &str) -> String {
mem::replace(&mut self.status, new.to_string())
}
// Sort le brouillon (s'il y en a un) et laisse None.
fn send_draft(&mut self) -> Option<String> {
self.draft.take()
}
}
fn main() {
let mut inbox = Inbox {
unread: vec!["On déjeune ?".to_string(), "Facture n°42".to_string()],
status: "en ligne".to_string(),
draft: Some("À ce soir, 18 h".to_string()),
};
let messages = inbox.read_all();
println!("lus {:?}, encore non lus : {:?}", messages, inbox.unread);
let old = inbox.set_status("en réunion");
println!("le statut était '{old}', maintenant '{}'", inbox.status);
println!("envoyé : {:?}", inbox.send_draft());
println!("envoyé à nouveau : {:?}", inbox.send_draft());
}

Sortie :

lus ["On déjeune ?", "Facture n°42"], encore non lus : []
le statut était 'en ligne', maintenant 'en réunion'
envoyé : Some("À ce soir, 18 h")
envoyé à nouveau : None
▶ Essayez par vous-même

Aucun clone() nulle part : les messages et l’ancien statut sont déplacés, pas copiés. C’est pour cela que ces fonctions sont à la fois sûres et peu coûteuses.

Deux cousines complètent la famille. mem::swap(&mut a, &mut b) échange deux valeurs, comme quand on intervertit deux assiettes sur une table. Option::replace(new) met une nouvelle valeur dans une Option et rend l’ancienne.

use std::mem;
fn main() {
// Remplir une liste pendant que l'autre est en cours de traitement.
let mut filling = vec!["commande 1", "commande 2"];
let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing);
println!("en traitement {processing:?}, en attente {filling:?}");
// Option::replace : met une nouvelle valeur et rend l'ancienne.
let mut now_playing = Some("Chanson A");
let previous = now_playing.replace("Chanson B");
println!("avant {previous:?}, maintenant {now_playing:?}");
}

Sortie :

en traitement ["commande 1", "commande 2"], en attente []
avant Some("Chanson A"), maintenant Some("Chanson B")
▶ Essayez par vous-même

À utiliser quand vous n’avez qu’un accès &mut, mais que vous devez posséder la valeur qui est dedans.

Voyez windows(2) comme une loupe qui glisse le long d’une règle : elle montre deux graduations à la fois, puis avance d’un cran. chunks(3) ressemble au rangement des œufs dans des boîtes de trois : la dernière boîte peut n’être qu’à moitié pleine. chunks_exact(3) ne vous donne que des boîtes pleines et met les œufs restants de côté dans .remainder().

Ces méthodes fonctionnent sur n’importe quelle slice (tranche), donc aussi sur les tableaux et les vecteurs.

fn main() {
let temps = [12, 15, 14, 19, 21];
// windows(2) : chaque paire de voisins, qui se chevauchent
for pair in temps.windows(2) {
println!("{} -> {} : variation {}", pair[0], pair[1], pair[1] - pair[0]);
}
// chunks(3) : des lots de 3, le dernier peut être plus petit
for batch in emails.chunks(3) {
println!("envoi à {batch:?}");
}
// chunks_exact(2) : uniquement des lots complets ; le reste attend dans remainder()
let seats = [1, 2, 3, 4, 5];
let pairs = seats.chunks_exact(2);
let leftover = pairs.remainder();
for pair in pairs {
println!("paire {pair:?}");
}
println!("reste : {leftover:?}");
}

Sortie :

12 -> 15 : variation 3
15 -> 14 : variation -1
14 -> 19 : variation 5
19 -> 21 : variation 2
paire [1, 2]
paire [3, 4]
reste : [5]
▶ Essayez par vous-même

Quelques autres méthodes de slice répondent à des questions courantes sans boucle. split_first() vous donne le premier élément et le reste, comme un professeur qui lit le premier nom d’une liste et tend le reste de la liste à un assistant. split_last() fait la même chose en partant de la fin.

fn main() {
let command = ["git", "commit", "-m", "corrige une coquille"];
// split_first : le premier élément, et une slice avec le reste
if let Some((program, args)) = command.split_first() {
println!("programme : {program}, arguments : {args:?}");
}
// split_last : le dernier élément, et tout ce qui le précède
if let Some((message, _rest)) = command.split_last() {
println!("message : {message}");
}
println!("c'est git ? {}", command.starts_with(&["git"]));
println!("contient -m ? {}", command.contains(&"-m"));
// iter().rev() : parcourir à l'envers sans modifier la slice
let history = ["ouvrir", "modifier", "enregistrer"];
let undo_order: Vec<_> = history.iter().rev().collect();
println!("ordre d'annulation : {undo_order:?}");
}

Sortie :

programme : git, arguments : ["commit", "-m", "corrige une coquille"]
message : corrige une coquille
c'est git ? true
contient -m ? true
ordre d'annulation : ["enregistrer", "modifier", "ouvrir"]
▶ Essayez par vous-même

split_first et split_last renvoient une Option, car une slice vide n’a pas de premier élément. C’est pour cela qu’on utilise if let.

À utiliser quand vous avez besoin des voisins (windows), de lots (chunks) ou de « la tête et le reste » (split_first).

Les méthodes d’itérateur que vous utiliserez tout le temps

Section intitulée « Les méthodes d’itérateur que vous utiliserez tout le temps »

La page Itérateurs présente map, filter, sum et collect. Voici les outils suivants de la boîte.

Imaginez que vous distribuez des cartes. Parfois, vous voulez voir la carte suivante avant de décider de la tirer. .peekable() vous donne un itérateur qui sait faire ça : .peek() montre l’élément suivant sans le prendre, et .next_if(...) ne le prend que s’il passe un test. C’est exactement ce dont les petits analyseurs (parsers) ont besoin.

fn main() {
let input = "12+345-6";
let mut chars = input.chars().peekable();
let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() {
if c.is_ascii_digit() {
let mut number = c.to_string();
// Prendre le caractère suivant seulement tant que c'est aussi un chiffre.
while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) {
number.push(d);
}
tokens.push(number);
} else {
tokens.push(c.to_string());
}
}
println!("{tokens:?}");
}

Sortie :

["12", "+", "345", "-", "6"]
▶ Essayez par vous-même

Sans peekable, vous liriez le + en cherchant d’autres chiffres, et il serait alors perdu.

À utiliser quand l’élément suivant décide de ce que vous faites de l’élément actuel.

Parcourir des listes : zip, enumerate, step_by, take_while, skip_while, chain

Section intitulée « Parcourir des listes : zip, enumerate, step_by, take_while, skip_while, chain »
Méthode Voyez-la comme À utiliser quand…
zip une fermeture éclair qui assemble deux côtés dent par dent vous avez deux listes qui vont ensemble
enumerate la numérotation des lignes d’une liste vous avez besoin de l’index en plus de l’élément
step_by(n) monter un escalier deux (ou n) marches à la fois vous voulez un élément sur n
take_while manger des bonbons jusqu’à tomber sur un que vous n’aimez pas vous voulez le début d’une liste, jusqu’au premier « non »
skip_while sauter les publicités jusqu’au début de l’émission vous voulez la suite, à partir du premier « non »
chain atteler deux wagons de train vous voulez une liste à la suite d’une autre
fn main() {
let names = ["Ana", "Ben", "Chen"];
let scores = [88, 92, 75];
// zip associe deux listes ; enumerate ajoute un compteur qui part de 0
for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() {
println!("{}. {name} : {score}", i + 1);
}
// step_by : toutes les 15 minutes dans l'heure
let quarters: Vec<u32> = (0..60).step_by(15).collect();
println!("quarts d'heure : {quarters:?}");
// into_iter() sur un tableau donne les nombres eux-mêmes
let queue = [3, 5, 8, 12, 4];
let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect();
let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect();
println!("avant le premier grand : {small:?}, à partir de là : {rest:?}");
// chain : une liste après l'autre
let morning = ["e-mails", "réunion d'équipe"];
let afternoon = ["relecture", "déploiement"];
let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect();
println!("{day:?}");
}

Sortie :

1. Ana : 88
2. Ben : 92
3. Chen : 75
quarts d'heure : [0, 15, 30, 45]
avant le premier grand : [3, 5, 8], à partir de là : [12, 4]
["e-mails", "réunion d'équipe", "relecture", "déploiement"]
▶ Essayez par vous-même

Remarquez que take_while s’arrête à 12 pour de bon. Le 4 qui suit est petit, mais il n’est jamais examiné. C’est la différence avec filter, qui regarde chaque élément.

Répondre à des questions : fold, position, find_map, partition, flat_map

Section intitulée « Répondre à des questions : fold, position, find_map, partition, flat_map »
Méthode Voyez-la comme À utiliser quand…
sum la touche « + » d’une calculatrice vous voulez juste le total
fold une boule de neige qui dévale une pente et grossit à chaque étape vous construisez un seul résultat à votre façon
position « à quelle place est assis Ben ? » vous avez besoin de l’index de la première correspondance
find_map essayer des clés jusqu’à ce que l’une ouvre la porte vous tentez quelque chose sur chaque élément et gardez la première réussite
partition trier le linge dans deux paniers vous coupez une liste en deux selon une question oui/non
flat_map vider plusieurs sacs sur une même table chaque élément devient plusieurs éléments
fn main() {
let orders = [12, 40, 7, 55, 30];
// sum : on additionne, tout simplement
let total: i32 = orders.iter().sum();
// fold : on additionne à sa façon (ici, chaque commande paie aussi 2 euros de frais)
let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2);
println!("total {total}, avec frais {with_fees}");
// position : l'index de la première correspondance
println!("la première commande au-dessus de 50 est à l'index {:?}", orders.iter().position(|p| *p > 50));
// partition : deux Vec, un pour « oui » et un pour « non »
let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30);
println!("grosses {big:?}, petites {small:?}");
// find_map : essayer de convertir chaque chaîne, garder la première qui marche
let inputs = ["abc", "", "42", "7"];
let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok());
println!("premier nombre : {first_number:?}");
// flat_map : chaque ligne devient plusieurs mots, tous dans une seule liste
let lines = ["acheter du lait", "appeler maman"];
let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect();
println!("{words:?}");
}

Sortie :

total 144, avec frais 154
la première commande au-dessus de 50 est à l'index Some(3)
grosses [40, 55, 30], petites [12, 7]
premier nombre : Some(42)
["acheter", "du", "lait", "appeler", "maman"]
▶ Essayez par vous-même

fold prend une valeur de départ (0) et une closure (fonction anonyme) qui reçoit « le résultat jusqu’ici » et l’élément suivant. Si vous avez seulement besoin d’un total, sum le dit plus clairement. Sortez fold quand la règle est la vôtre.

Ces deux-là sont des méthodes de Vec, pas des méthodes d’itérateur, mais elles ont leur place dans le même tiroir. dedup est comme un DJ qui ne passe jamais deux fois de suite le même morceau (le morceau peut revenir plus tard). retain est un videur : seuls les éléments qui passent le test restent à l’intérieur.

fn main() {
// dedup supprime les répétitions qui se suivent
let mut clicks = vec!["accueil", "accueil", "boutique", "boutique", "boutique", "panier", "accueil"];
clicks.dedup();
println!("{clicks:?}");
// retain garde seulement les éléments qui passent le test
let mut cart = vec![("pommes", 3), ("pain", 0), ("lait", 1), ("œufs", 0)];
cart.retain(|(_, qty)| *qty > 0);
println!("{cart:?}");
}

Sortie :

["accueil", "boutique", "panier", "accueil"]
[("pommes", 3), ("lait", 1)]
▶ Essayez par vous-même

Le dernier "accueil" a survécu, car il n’était pas à côté d’un autre "accueil". Pour supprimer tous les doublons, triez d’abord, comme le montre la page Trier.

À utiliser quand vous voulez modifier un Vec sans en construire un nouveau.

Une Option ressemble à un colis qui est peut-être arrivé, ou peut-être pas. Vous pourriez ouvrir chaque colis avec un match complet, mais la plupart du temps, une petite méthode d’un mot dit la même chose. La page Option présente unwrap_or et match ; voici les aides suivantes.

fn main() {
let nickname: Option<String> = None;
let age: Option<u32> = Some(17);
let width: Option<u32> = Some(3);
let height: Option<u32> = Some(4);
// unwrap_or_default : la valeur, ou la valeur par défaut du type ("", 0, Vec vide...)
println!("surnom : '{}'", nickname.unwrap_or_default());
// map_or : une valeur de secours si None, sinon on transforme la valeur
println!("{}", age.map_or("âge inconnu".to_string(), |a| format!("{a} ans")));
// filter : garder la valeur seulement si elle passe un test
println!("âge adulte : {:?}", age.filter(|a| *a >= 18));
// zip : Some seulement si les DEUX sont Some
println!("surface : {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then : enchaîner une étape qui peut elle-même donner None
let good = Some("42").and_then(|s| s.parse::<u32>().ok());
let bad = Some("quarante").and_then(|s| s.parse::<u32>().ok());
println!("converti : {good:?} et {bad:?}");
}

Sortie :

surnom : ''
17 ans
âge adulte : None
surface : Some(12)
converti : Some(42) et None
▶ Essayez par vous-même

Dans une fonction, ? marche aussi sur une Option : si la valeur est None, la fonction renvoie None immédiatement. Dans une fonction qui renvoie un Result, transformez d’abord l’Option en Result avec ok_or(error) ou ok_or_else(|| error). Préférez ok_or_else quand construire l’erreur demande du travail, car elle ne s’exécute qu’en cas de besoin.

struct Employee {
name: String,
manager: Option<String>,
}
// `?` sur une Option : si c'est None, on renvoie None tout de suite.
fn manager_initial(e: &Employee) -> Option<char> {
let manager = e.manager.as_ref()?;
manager.chars().next()
}
// ok_or_else transforme None en erreur, pour que `?` marche dans une fonction qui renvoie un Result.
fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting.ok_or_else(|| "PORT n'est pas défini".to_string())?;
raw.parse::<u16>().map_err(|e| e.to_string())
}
fn main() {
let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) };
let grace = Employee { name: "Grace".to_string(), manager: None };
println!("{} : {:?}", ana.name, manager_initial(&ana));
println!("{} : {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080")));
println!("{:?}", port_from(None));
println!("{:?}", port_from(Some("quatre-vingts")));
// transpose : Option<Result<..>> devient Result<Option<..>>
let retries_setting: Option<&str> = Some("3");
let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose();
println!("{retries:?}");
}

Sortie :

Ana : Some('G')
Grace : None
Ok(8080)
Err("PORT n'est pas défini")
Err("invalid digit found in string")
Ok(Some(3))
▶ Essayez par vous-même

Le message invalid digit found in string (« chiffre invalide dans la chaîne ») vient de Rust lui-même, c’est pourquoi il reste en anglais. transpose sert rarement, mais c’est la réponse élégante à « j’ai un réglage facultatif, et sa conversion peut échouer ». Avec lui, vous pouvez mettre ? sur le résultat et garder l’Option.

À utiliser quand un match sur une Option a une branche intéressante et une branche sans intérêt.

Pensez à un vestiaire. Vous donnez un nom, et la personne à l’accueil trouve le crochet correspondant ou en installe un nouveau, puis vous le tend. Un seul aller, pas deux. C’est ça, .entry(key). La page HashMap montre entry(key).or_insert(value), le compteur classique. Deux autres variantes valent le coup :

  • or_insert_with(f) ne construit la valeur par défaut que si la clé manque. or_default() fait pareil avec la valeur par défaut du type.
  • and_modify(f) modifie la valeur si la clé existe ; enchaînez or_insert derrière pour le cas « nouvelle clé ».
use std::collections::HashMap;
fn main() {
let sales = [("pommes", 3), ("poires", 2), ("pommes", 5), ("prunes", 1), ("poires", 4)];
// and_modify + or_insert : mettre à jour si présent, insérer sinon
let mut totals: HashMap<&str, i32> = HashMap::new();
for (fruit, qty) in sales {
totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty);
}
// or_insert_with : créer le Vec vide seulement quand on en a besoin
let mut orders: HashMap<&str, Vec<i32>> = HashMap::new();
for (fruit, qty) in sales {
orders.entry(fruit).or_insert_with(Vec::new).push(qty);
}
// L'ordre d'une HashMap est aléatoire, donc on trie les clés avant d'afficher
let mut fruits: Vec<&str> = totals.keys().copied().collect();
fruits.sort();
for fruit in fruits {
println!("{fruit} : total {}, commandes {:?}", totals[fruit], orders[fruit]);
}
}

Sortie :

poires : total 6, commandes [2, 4]
pommes : total 8, commandes [3, 5]
prunes : total 1, commandes [1]
▶ Essayez par vous-même

À utiliser quand vous écririez sinon « si la clé existe, modifie-la, sinon insère-la », ce qui cherche la clé deux fois.

Certaines routes sont à sens unique : une fois engagé, vous n’en ressortez jamais. En Rust, une expression qui ne se termine jamais a le type never (le type « jamais »), noté !. panic!, return, break, continue, une loop sans fin et std::process::exit ont tous ce type.

Comme une valeur de type ! ne peut jamais exister pour de vrai, le compilateur la laisse remplacer n’importe quel type. Voilà pourquoi un match peut avoir une branche qui donne un i32 et une autre qui fait return : la deuxième branche ne produit jamais de valeur, elle ne peut donc pas casser les types.

use std::process;
// `-> !` signifie « cette fonction ne revient jamais »
fn quit(message: &str) -> ! {
println!("{message}");
process::exit(0)
}
fn level_points(level: &str) -> u32 {
let points: u32 = match level {
"facile" => 10,
"difficile" => 30,
_ => return 0, // `return` a le type `!`, donc il convient là où un u32 est attendu
};
points * 2
}
fn main() {
println!("facile : {}, difficile : {}, bizarre : {}", level_points("facile"), level_points("difficile"), level_points("bizarre"));
let mut total = 0;
for input in ["4", "oups", "6"] {
let n: i32 = match input.parse() {
Ok(n) => n,
Err(_) => continue, // `continue` est aussi de type `!`
};
total += n;
}
println!("total : {total}");
let config_file: Option<&str> = None;
let config = match config_file {
Some(path) => path,
None => quit("aucun fichier de configuration trouvé, arrêt"),
};
println!("chargé : {config}"); // jamais affiché
}

Sortie :

facile : 20, difficile : 60, bizarre : 0
total : 10
aucun fichier de configuration trouvé, arrêt
▶ Essayez par vous-même

Une mise en garde : process::exit arrête le programme immédiatement, sans exécuter le code de nettoyage (les destructeurs) des valeurs encore en vie. Dans un vrai programme, revenir de main est en général le choix le plus doux.

Deux macros utilisent ! pour servir de bouche-trou pendant que vous esquissez votre code. todo!() veut dire « j’écrirai ça plus tard ». unimplemented!() veut dire « ce n’est pas pris en charge ici ». Les deux déclenchent une panic si on les atteint, mais elles permettent au reste de votre programme de compiler :

fn export_pdf(report: &str) -> Vec<u8> {
todo!("export PDF prévu pour la v2")
}

Si vous l’appelez, vous obtenez not yet implemented: export PDF prévu pour la v2. Avec unimplemented!("export PDF"), le message devient not implemented: export PDF. Le début du message vient de Rust, il reste donc en anglais.

À utiliser quand une fonction doit arrêter le programme, ou qu’une branche doit sortir plus tôt au lieu de donner une valeur.

Ce sont les outils de poche : minuscules, mais vous vous en servirez chaque semaine.

  • matches!(value, pattern) répond à « est-ce que ça correspond au motif ? » par true ou false (plus de détails sur la page Pattern matching).
  • std::cmp::min / max choisissent la plus petite ou la plus grande de deux valeurs, et .clamp(low, high) garde une valeur dans un intervalle, comme un bouton de volume qui s’arrête à 100.
  • .abs_diff(other) donne la distance entre deux nombres. Elle est toujours positive et ne déborde jamais, même pour les types non signés.
  • dbg!(expr) affiche le fichier, la ligne, l’expression et la valeur, puis rend la valeur : vous pouvez donc l’enrouler autour de n’importe quoi.
  • assert!(condition) déclenche une panic si la condition est fausse. debug_assert! fait la même chose, mais seulement dans les builds de debug : il ne coûte rien dans les builds de release.
use std::cmp::{max, min};
enum Order {
Pending,
Shipped,
Delivered,
Cancelled,
}
fn main() {
let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches! : un test oui/non sur un motif, avec | pour « ou »
let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count();
println!("commandes en cours : {open}");
// min, max et clamp
println!("min {}, max {}", min(7, 3), max(7, 3));
let volume = 140;
let safe = volume.clamp(0, 100);
println!("volume {volume} ramené à {safe}");
// abs_diff : l'écart, jamais négatif
let (guess, secret): (u32, u32) = (30, 42);
println!("vous êtes à {} près", guess.abs_diff(secret));
// dbg! écrit sur stderr et rend la valeur
let doubled = dbg!(safe * 2);
// assert! arrête le programme si ceci devient faux
assert!(doubled <= 200, "volume doublé trop élevé : {doubled}");
debug_assert!(safe <= 100); // vérifié seulement dans les builds de debug
println!("toutes les vérifications sont passées");
}

Sortie :

commandes en cours : 3
min 3, max 7
volume 140 ramené à 100
vous êtes à 12 près
toutes les vérifications sont passées
▶ Essayez par vous-même

La ligne de dbg! n’apparaît pas dans la sortie ci-dessus, parce que dbg! écrit sur stderr (la sortie d’erreur), pas sur stdout. Dans le Playground, elle s’affiche dans le panneau « Standard Error » :

[src/main.rs:28:19] safe * 2 = 200

C’est aussi pour cela que vous pouvez semer des dbg! sans crainte pendant le débogage : ils ne se mélangent pas à la vraie sortie de votre programme. Pensez simplement à les retirer avant de livrer.

Le tri a lui aussi ses petits outils. La page Trier les détaille ; en voici la version courte. sort_unstable est un peu plus rapide que sort, et convient très bien pour de simples nombres. sort_by_key trie selon un champ. binary_search retrouve très vite un élément dans des données triées, comme quand on ouvre un dictionnaire au milieu au lieu de le lire depuis la première page.

struct Player {
name: &'static str,
score: u32,
}
fn main() {
// sort_unstable : un peu plus rapide ; des éléments égaux peuvent changer de place
let mut ids = vec![42, 7, 19, 3, 25];
ids.sort_unstable();
println!("{ids:?}");
// binary_search : Ok(index) si trouvé, Err(index) là où il irait
println!("chercher 19 : {:?}", ids.binary_search(&19));
println!("chercher 20 : {:?}", ids.binary_search(&20));
// sort_by_key : trier selon un champ
let mut players = vec![
Player { name: "Ana", score: 120 },
Player { name: "Ben", score: 95 },
Player { name: "Chen", score: 150 },
];
players.sort_by_key(|p| p.score);
let names: Vec<&str> = players.iter().map(|p| p.name).collect();
println!("du plus faible au plus fort : {names:?}");
}

Sortie :

[3, 7, 19, 25, 42]
chercher 19 : Ok(2)
chercher 20 : Err(3)
du plus faible au plus fort : ["Ben", "Ana", "Chen"]
▶ Essayez par vous-même

Err(3) n’est pas un échec dont il faut s’inquiéter. Il vous dit que 20 n’est pas dans la liste, et que l’index 3 est l’endroit où vous pourriez l’insérer pour que la liste reste triée.

À utiliser quand vous vous surprenez à écrire un petit if ou une petite boucle que l’une de ces méthodes fait déjà.

Utiliser mem::take sur un type sans Default. mem::take doit laisser quelque chose derrière lui, il a donc besoin de savoir ce que « vide » veut dire pour ce type :

use std::mem;
struct Connection {
host: String,
}
struct Pool {
active: Connection,
}
impl Pool {
fn reset(&mut self) -> Connection {
mem::take(&mut self.active) // ❌ à quoi ressemblerait une Connection « vide » ?
}
}
error[E0277]: the trait bound `Connection: Default` is not satisfied
--> src/main.rs:13:19
|
13 | mem::take(&mut self.active) // ❌ à quoi ressemblerait une Connection « vide » ?
| --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection`
| |
| required by a bound introduced by this call
|
help: consider annotating `Connection` with `#[derive(Default)]`

Le compilateur vous dit que le trait Default n’est pas implémenté pour Connection. Corrigez en choisissant vous-même le remplaçant avec mem::replace(&mut self.active, Connection { host: "db2".to_string() }), ou en stockant le champ sous forme d’Option<Connection> et en utilisant self.active.take(). N’ajoutez #[derive(Default)] que si une valeur « vide » a vraiment du sens pour ce type.

Utiliser ? sur une Option dans une fonction qui renvoie un Result. Les deux ne se mélangent pas automatiquement :

fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting?; // ❌ None n'a aucun message d'erreur à renvoyer
raw.parse::<u16>().map_err(|e| e.to_string())
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
--> src/main.rs:2:22
|
1 | fn port_from(setting: Option<&str>) -> Result<u16, String> {
| ---------------------------------------------------------- this function returns a `Result`
2 | let raw = setting?; // ❌ None n'a aucun message d'erreur à renvoyer
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`

Le compilateur vous donne déjà la solution : setting.ok_or("PORT n'est pas défini".to_string())?, ou ok_or_else comme dans l’exemple plus haut. Consultez E0277 pour d’autres formes de cette erreur.

S’attendre à ce que dedup ou binary_search marchent sur des données non triées. dedup ne supprime que les répétitions qui se suivent, et binary_search sur des données non triées donne une réponse sans aucun sens. Aucun des deux ne provoque d’erreur de compilation, c’est donc facile à rater. Triez d’abord.

Oublier les restes de chunks_exact. Une boucle for sur chunks_exact(3) saute en silence les 1 ou 2 derniers éléments. S’ils comptent, traitez .remainder(), ou utilisez plutôt chunks.

Vous avez les prix [100, 104, 98, 110, 107]. Construisez un Vec avec la variation d’un jour au suivant, affichez-le, puis affichez la plus forte hausse. Indice : windows(2) donne chaque paire sous forme d’une petite slice w, la variation vaut donc w[1] - w[0]. .iter().max() trouve la plus grande.

Voir la solution
fn main() {
let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect();
println!("variations : {changes:?}");
let biggest_rise = changes.iter().max();
println!("plus forte hausse : {biggest_rise:?}");
}

Sortie :

variations : [4, -6, 12, -3]
plus forte hausse : Some(12)
▶ Essayez par vous-même

Cinq prix donnent quatre variations, car windows(2) forme une paire à chaque pas. max() renvoie une Option, puisqu’une liste vide n’a pas de plus grande valeur.

Écrivez une struct Printer avec un champ queue: Vec<String>. Donnez-lui une méthode add(&mut self, file: &str) et une méthode flush(&mut self) -> Vec<String> qui rend tous les fichiers en attente et laisse la file vide. Ajoutez deux fichiers, videz la file, puis affichez ce qui a été envoyé et le nombre de fichiers restants. Indice : flush n’a que &mut self, utilisez donc std::mem::take.

Voir la solution
use std::mem;
struct Printer {
queue: Vec<String>,
}
impl Printer {
fn add(&mut self, file: &str) {
self.queue.push(file.to_string());
}
fn flush(&mut self) -> Vec<String> {
mem::take(&mut self.queue)
}
}
fn main() {
let mut printer = Printer { queue: Vec::new() };
printer.add("rapport.pdf");
printer.add("photo.png");
let jobs = printer.flush();
println!("impression de {} documents : {jobs:?}", jobs.len());
println!("restant dans la file : {}", printer.queue.len());
}

Sortie :

impression de 2 documents : ["rapport.pdf", "photo.png"]
restant dans la file : 0
▶ Essayez par vous-même

Écrire self.queue tout seul dans flush échouerait avec E0507. mem::take sort le Vec et en laisse un vide à la place, sans aucune copie.

Écrivez une fonction first_port(candidates: &[&str]) -> Option<u16> qui renvoie la première chaîne qui se convertit en u16. Appelez-la avec ["", "abc", "70000", "8080", "9090"] puis avec ["aucun", "-1"], et affichez le port, ou 3000 s’il n’y en a pas. Indice : find_map avec s.parse::<u16>().ok(), puis unwrap_or(3000).

Voir la solution
fn first_port(candidates: &[&str]) -> Option<u16> {
candidates.iter().find_map(|s| s.parse::<u16>().ok())
}
fn main() {
let from_config = ["", "abc", "70000", "8080", "9090"];
let all_bad = ["aucun", "-1"];
println!("port utilisé : {}", first_port(&from_config).unwrap_or(3000));
println!("port utilisé : {}", first_port(&all_bad).unwrap_or(3000));
}

Sortie :

port utilisé : 8080
port utilisé : 3000
▶ Essayez par vous-même

"70000" ressemble à un nombre, mais il ne tient pas dans un u16 (le maximum est 65535) : la conversion échoue et find_map passe au suivant. Il s’arrête sur "8080" et ne regarde même pas "9090".

  • mem::take, mem::replace et Option::take sortent une valeur d’un &mut en laissant quelque chose à sa place ; mem::swap échange deux valeurs.
  • Sur les slices, windows donne des voisins qui se chevauchent, chunks donne des lots, et split_first / split_last donnent « un élément et le reste ».
  • peekable vous laisse regarder en avance ; zip, enumerate, chain, fold, find_map et partition remplacent bien des boucles écrites à la main.
  • Les aides d’Option comme unwrap_or_default, map_or, filter et ok_or vous évitent d’écrire de petits match, et ? marche aussi sur Option.
  • entry() avec or_insert_with ou and_modify met à jour une HashMap en une seule recherche.
  • Le type never ! appartient au code qui ne se termine jamais (panic!, return, continue, process::exit), il convient donc partout où une valeur est attendue.

Petit quiz

1. Que laisse mem::take(&mut self.items) derrière lui quand items est un Vec ?

2. Que vous donne [1, 2, 3, 4].windows(2) ?

3. Pourquoi let x: i32 = match s { "a" => 1, _ => return }; compile-t-il ?

4. Votre fonction renvoie un Result, mais vous avez une Option. Qu'ajoutez-vous avant ? ?

Score : 0 / 4