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 (unVecvide, uneStringvide,0,None).mem::replace(&mut x, new)metnewà la place et vous rend l’ancienne valeur.Option::take()prend ce qu’il y a dans uneOptionet laisseNone.
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 : NoneAucun 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")À utiliser quand vous n’avez qu’un accès &mut, mais que vous devez posséder la valeur qui est dedans.
Les slices : windows, chunks et compagnie
Section intitulée « Les slices : windows, chunks et compagnie »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 let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; 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 315 -> 14 : variation -114 -> 19 : variation 519 -> 21 : variation 2envoi à ["[email protected]", "[email protected]", "[email protected]"]envoi à ["[email protected]", "[email protected]"]paire [1, 2]paire [3, 4]reste : [5]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 coquillec'est git ? truecontient -m ? trueordre d'annulation : ["enregistrer", "modifier", "ouvrir"]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.
peekable : regarder avant de prendre
Section intitulée « peekable : regarder avant de prendre »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"]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 : 882. Ben : 923. Chen : 75quarts 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"]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 154la 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"]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.
Nettoyer un Vec sur place : dedup et retain
Section intitulée « Nettoyer un Vec sur place : dedup et retain »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)]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.
Les raccourcis d’Option et de Result
Section intitulée « Les raccourcis d’Option et de Result »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 : Nonesurface : Some(12)converti : Some(42) et NoneDans 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 : NoneOk(8080)Err("PORT n'est pas défini")Err("invalid digit found in string")Ok(Some(3))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.
L’API entry de HashMap
Section intitulée « L’API entry de HashMap »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înezor_insertderriè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]À utiliser quand vous écririez sinon « si la clé existe, modifie-la, sinon insère-la », ce qui cherche la clé deux fois.
Le type never ! : du code qui ne revient jamais
Section intitulée « Le type never ! : du code qui ne revient jamais »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 : 0total : 10aucun fichier de configuration trouvé, arrêtUne 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.
Petits gains de confort
Section intitulée « Petits gains de confort »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 ? » partrueoufalse(plus de détails sur la page Pattern matching).std::cmp::min/maxchoisissent 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 : 3min 3, max 7volume 140 ramené à 100vous êtes à 12 prèstoutes les vérifications sont passéesLa 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 = 200C’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"]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à.
Erreurs courantes
Section intitulée « Erreurs courantes »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.
Exercices
Section intitulée « Exercices »1. Les variations de prix au jour le jour
Section intitulée « 1. Les variations de prix au jour le jour »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)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.
2. Vider la file d’impression
Section intitulée « 2. Vider la file d’impression »É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É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.
3. Le premier port valide
Section intitulée « 3. Le premier port valide »É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é : 8080port utilisé : 3000"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".
Récapitulatif
Section intitulée « Récapitulatif »mem::take,mem::replaceetOption::takesortent une valeur d’un&muten laissant quelque chose à sa place ;mem::swapéchange deux valeurs.- Sur les slices,
windowsdonne des voisins qui se chevauchent,chunksdonne des lots, etsplit_first/split_lastdonnent « un élément et le reste ». peekablevous laisse regarder en avance ;zip,enumerate,chain,fold,find_mapetpartitionremplacent bien des boucles écrites à la main.- Les aides d’
Optioncommeunwrap_or_default,map_or,filteretok_orvous évitent d’écrire de petitsmatch, et?marche aussi surOption. entry()avecor_insert_withouand_modifymet à jour uneHashMapen 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
Section intitulée « Petit quiz »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
Pour aller plus loin
Section intitulée « Pour aller plus loin »std::mem— documentation de la bibliothèque standard (en anglais) :take,replace,swapet les autres outils de gestion de la mémoire, avec des exemples.- Méthodes des slices — documentation de la bibliothèque standard (en anglais) : toutes les méthodes de slice, dont
windows,chunks,split_firstetbinary_search. - Le trait
Iterator— documentation de la bibliothèque standard (en anglais) : la liste complète des méthodes d’itérateur, dezipetpeekableàpartitionetfind_map. Option— documentation de la bibliothèque standard (en anglais) : toutes les aides d’Option, regroupées par usage.- Diverging functions — Rust by Example (en anglais) : de courts exemples de fonctions qui renvoient
!, et pourquoi c’est utile.