Idiomi della std di Rust: piccoli trucchi di tutti i giorni
In ogni cucina c’è un cassetto pieno di piccoli attrezzi: il pelapatate, l’apribottiglie, lo spremiaglio. Potresti fare tutto con un grosso coltello, ma i piccoli attrezzi rendono il lavoro più rapido e più sicuro. Anche la libreria standard di Rust ha un cassetto così.
Questa pagina è un giro in quel cassetto. Ogni trucco è breve: un’immagine dalla vita reale, un esempio eseguibile e una riga «usalo quando…», così sai quando tirarlo fuori. Non devi impararli tutti insieme. Dai un’occhiata alla pagina adesso, e torna qui quando nel tuo codice vedi uno schema che ti sembra goffo.
Estrarre un valore da un &mut: mem::take, replace, swap
Sezione intitolata “Estrarre un valore da un &mut: mem::take, replace, swap”Immagina un bidone della spazzatura con dentro un sacchetto. Puoi tirare fuori il sacchetto pieno, ma solo se ne metti subito uno nuovo. Il bidone non resta mai senza sacchetto.
Rust ha la stessa regola. Se hai solo &mut self, non puoi semplicemente spostare fuori un campo, perché lasceresti un buco nella struct. Il compilatore ti ferma con E0507. Le funzioni di std::mem risolvono il problema lasciando sempre qualcosa al suo posto:
mem::take(&mut x)prende il valore e lascia il valore predefinito del tipo (unVecvuoto, unaStringvuota,0,None).mem::replace(&mut x, nuovo)mette dentronuovoe ti restituisce il vecchio valore.Option::take()prende ciò che c’è dentro unOptione lasciaNone.
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // Consegna tutti i messaggi non letti e lascia al loro posto un Vec vuoto. fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// Metti un nuovo stato e riprenditi quello vecchio. fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// Tira fuori la bozza (se c'è) e lascia None. fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["Pranzo?".to_string(), "Fattura #42".to_string()], status: "online".to_string(), draft: Some("Ci vediamo alle 6".to_string()), };
let messages = inbox.read_all(); println!("letti {:?}, ancora da leggere: {:?}", messages, inbox.unread);
let old = inbox.set_status("in riunione"); println!("lo stato era '{old}', ora '{}'", inbox.status);
println!("inviata: {:?}", inbox.send_draft()); println!("inviata di nuovo: {:?}", inbox.send_draft());}Output:
letti ["Pranzo?", "Fattura #42"], ancora da leggere: []lo stato era 'online', ora 'in riunione'inviata: Some("Ci vediamo alle 6")inviata di nuovo: NoneNessun clone() da nessuna parte: i messaggi e il vecchio stato vengono spostati fuori, non copiati. Ecco perché queste funzioni sono sia sicure sia economiche.
Due parenti completano il set. mem::swap(&mut a, &mut b) scambia due valori, come scambiare due piatti in tavola. Option::replace(nuovo) mette un nuovo valore in un Option e restituisce quello vecchio.
use std::mem;
fn main() { // Riempi una lista mentre l'altra viene lavorata. let mut filling = vec!["ordine 1", "ordine 2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("in lavorazione {processing:?}, in riempimento {filling:?}");
// Option::replace: metti dentro un nuovo valore, riprenditi quello vecchio. let mut now_playing = Some("Canzone A"); let previous = now_playing.replace("Canzone B"); println!("prima {previous:?}, ora {now_playing:?}");}Output:
in lavorazione ["ordine 1", "ordine 2"], in riempimento []prima Some("Canzone A"), ora Some("Canzone B")Usalo quando hai solo un accesso &mut ma ti serve possedere il valore che c’è dentro.
Slice: windows, chunks e compagnia
Sezione intitolata “Slice: windows, chunks e compagnia”Pensa a windows(2) come a una lente d’ingrandimento che scorre lungo un righello: mostra due tacche alla volta, poi si sposta di un passo. chunks(3) è come mettere le uova in confezioni da tre: l’ultima confezione può essere piena solo in parte. chunks_exact(3) ti dà solo confezioni piene e tiene da parte le uova avanzate in .remainder().
Questi metodi funzionano su qualsiasi slice, quindi anche su array e vettori.
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2): ogni coppia di vicini, sovrapposte for pair in temps.windows(2) { println!("{} -> {}: variazione {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3): gruppi da 3, l'ultimo può essere più piccolo let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("invio a {batch:?}"); }
// chunks_exact(2): solo gruppi completi; il resto aspetta in remainder() let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("coppia {pair:?}"); } println!("avanzo: {leftover:?}");}Output:
12 -> 15: variazione 315 -> 14: variazione -114 -> 19: variazione 519 -> 21: variazione 2invio a ["[email protected]", "[email protected]", "[email protected]"]invio a ["[email protected]", "[email protected]"]coppia [1, 2]coppia [3, 4]avanzo: [5]Qualche altro helper delle slice risponde a domande comuni senza bisogno di un ciclo. split_first() ti dà il primo elemento e il resto, come un’insegnante che legge il primo nome dell’elenco e passa il resto a un aiutante. split_last() fa lo stesso partendo dalla fine.
fn main() { let command = ["git", "commit", "-m", "correggi refuso"];
// split_first: il primo elemento e una slice con il resto if let Some((program, args)) = command.split_first() { println!("programma: {program}, argomenti: {args:?}"); }
// split_last: l'ultimo elemento e tutto ciò che viene prima if let Some((message, _rest)) = command.split_last() { println!("messaggio: {message}"); }
println!("è git? {}", command.starts_with(&["git"])); println!("ha -m? {}", command.contains(&"-m"));
// iter().rev(): percorri all'indietro senza modificare la slice let history = ["apri", "modifica", "salva"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("ordine di annullamento: {undo_order:?}");}Output:
programma: git, argomenti: ["commit", "-m", "correggi refuso"]messaggio: correggi refusoè git? trueha -m? trueordine di annullamento: ["salva", "modifica", "apri"]split_first e split_last restituiscono un Option, perché una slice vuota non ha un primo elemento. Ecco perché usiamo if let.
Usalo quando ti servono i vicini (windows), dei gruppi (chunks) o «la testa e il resto» (split_first).
Helper degli iteratori che userai sempre
Sezione intitolata “Helper degli iteratori che userai sempre”La pagina sugli iteratori tratta map, filter, sum e collect. Ecco i prossimi attrezzi della cassetta.
peekable: guarda prima di prendere
Sezione intitolata “peekable: guarda prima di prendere”Immagina di distribuire le carte. A volte vuoi guardare la carta successiva prima di decidere se pescarla. .peekable() ti dà un iteratore capace di farlo: .peek() mostra l’elemento successivo senza prenderlo, e .next_if(...) lo prende solo se supera un test. È proprio ciò che serve ai piccoli parser.
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(); // Prendi il carattere successivo solo finché è anche lui una cifra. 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:?}");}Output:
["12", "+", "345", "-", "6"]Senza peekable, leggeresti il + mentre cerchi altre cifre, e a quel punto sarebbe perso.
Usalo quando l’elemento successivo decide cosa fare con quello attuale.
Scorrere liste: zip, enumerate, step_by, take_while, skip_while, chain
Sezione intitolata “Scorrere liste: zip, enumerate, step_by, take_while, skip_while, chain”| Metodo | Pensalo come | Usalo quando… |
|---|---|---|
zip |
una cerniera che unisce due lati dente per dente | hai due liste che vanno insieme |
enumerate |
numerare le righe di un elenco | ti serve l’indice oltre all’elemento |
step_by(n) |
salire le scale due (o n) gradini alla volta | vuoi un elemento ogni n |
take_while |
mangiare caramelle finché non ne trovi una che non ti piace | vuoi l’inizio di una lista, fino al primo «no» |
skip_while |
saltare la pubblicità finché non inizia il programma | vuoi il resto, dal primo «no» in poi |
chain |
agganciare due vagoni del treno | vuoi una lista dopo l’altra |
fn main() { let names = ["Ana", "Ben", "Chen"]; let scores = [88, 92, 75];
// zip accoppia due liste; enumerate aggiunge un contatore che parte da 0 for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by: un minuto ogni 15 nell'ora let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("quarti d'ora: {quarters:?}");
// into_iter() su un array dà i numeri stessi 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!("prima del primo grande: {small:?}, da lì in poi: {rest:?}");
// chain: una lista dopo l'altra let morning = ["email", "riunione"]; let afternoon = ["revisione", "rilascio"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}Output:
1. Ana: 882. Ben: 923. Chen: 75quarti d'ora: [0, 15, 30, 45]prima del primo grande: [3, 5, 8], da lì in poi: [12, 4]["email", "riunione", "revisione", "rilascio"]Nota che take_while si ferma a 12 per sempre. Il 4 dopo è piccolo, ma non viene mai controllato. È questa la differenza con filter, che guarda ogni elemento.
Rispondere a domande: fold, position, find_map, partition, flat_map
Sezione intitolata “Rispondere a domande: fold, position, find_map, partition, flat_map”| Metodo | Pensalo come | Usalo quando… |
|---|---|---|
sum |
il tasto più della calcolatrice | vuoi solo il totale |
fold |
una palla di neve che rotola giù e cresce a ogni passo | costruisci un risultato a modo tuo |
position |
«a che numero di posto è seduto Ben?» | ti serve l’indice della prima corrispondenza |
find_map |
provare chiavi finché una apre la porta | provi qualcosa su ogni elemento e tieni il primo successo |
partition |
dividere il bucato in due cesti | dividi una lista in due con una domanda sì/no |
flat_map |
svuotare diverse borse sullo stesso tavolo | ogni elemento diventa più elementi |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum: semplicemente li somma let total: i32 = orders.iter().sum();
// fold: li somma a modo tuo (qui ogni ordine paga anche 2 euro di commissione) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("totale {total}, con commissioni {with_fees}");
// position: l'indice della prima corrispondenza println!("il primo ordine sopra 50 è all'indice {:?}", orders.iter().position(|p| *p > 50));
// partition: due Vec, uno per i «sì» e uno per i «no» let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("grandi {big:?}, piccoli {small:?}");
// find_map: prova a convertire ognuno, tieni il primo che funziona let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("primo numero: {first_number:?}");
// flat_map: ogni riga diventa più parole, tutte in un'unica lista let lines = ["compra latte", "chiama la mamma"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}Output:
totale 144, con commissioni 154il primo ordine sopra 50 è all'indice Some(3)grandi [40, 55, 30], piccoli [12, 7]primo numero: Some(42)["compra", "latte", "chiama", "la", "mamma"]fold prende un valore di partenza (0) e una closure che riceve «il risultato finora» e l’elemento successivo. Se ti serve solo un totale semplice, sum lo dice in modo più chiaro. Usa fold quando la regola è tutta tua.
Ripulire un Vec sul posto: dedup e retain
Sezione intitolata “Ripulire un Vec sul posto: dedup e retain”Questi due sono metodi di Vec, non degli iteratori, ma stanno nello stesso cassetto. dedup è come un DJ che non mette mai la stessa canzone due volte di fila (la canzone può tornare più tardi). retain è un buttafuori: restano dentro solo gli elementi che superano il test.
fn main() { // dedup toglie le ripetizioni che stanno una accanto all'altra let mut clicks = vec!["home", "home", "negozio", "negozio", "negozio", "carrello", "home"]; clicks.dedup(); println!("{clicks:?}");
// retain tiene solo gli elementi che superano il test let mut cart = vec![("mele", 3), ("pane", 0), ("latte", 1), ("uova", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}Output:
["home", "negozio", "carrello", "home"][("mele", 3), ("latte", 1)]L’ultimo "home" è sopravvissuto perché non era accanto a un altro "home". Per togliere ogni doppione, prima ordina, come mostra la pagina sull’ordinamento.
Usalo quando vuoi modificare un Vec senza costruirne uno nuovo.
Comodità di Option e Result
Sezione intitolata “Comodità di Option e Result”Un Option è come un pacco che potrebbe essere arrivato oppure no. Potresti aprire ogni pacco con un match completo, ma la maggior parte delle volte un helper di una parola dice la stessa cosa. La pagina su Option tratta unwrap_or e match; ecco gli helper successivi.
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: il valore, o il predefinito del tipo ("", 0, Vec vuoto...) println!("soprannome: '{}'", nickname.unwrap_or_default());
// map_or: un ripiego se è None, altrimenti trasforma il valore println!("{}", age.map_or("età sconosciuta".to_string(), |a| format!("{a} anni")));
// filter: tieni il valore solo se supera un test println!("età da adulto: {:?}", age.filter(|a| *a >= 18));
// zip: Some solo se ENTRAMBI sono Some println!("area: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: concatena un passaggio che a sua volta può dare None let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("quaranta").and_then(|s| s.parse::<u32>().ok()); println!("convertiti: {good:?} e {bad:?}");}Output:
soprannome: ''17 annietà da adulto: Nonearea: Some(12)convertiti: Some(42) e NoneDentro una funzione, ? funziona anche su Option: se il valore è None, la funzione restituisce subito None. In una funzione che restituisce un Result, trasforma prima l’Option in un Result con ok_or(errore) o ok_or_else(|| errore). Usa ok_or_else quando costruire l’errore richiede lavoro, perché viene eseguito solo quando serve.
struct Employee { name: String, manager: Option<String>,}
// `?` su un Option: se è None, restituisci subito None.fn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else trasforma None in un errore, così `?` funziona in una funzione con Result.fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "PORT non è impostata".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("ottanta")));
// transpose: Option<Result<..>> diventa Result<Option<..>> let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}Output:
Ana: Some('G')Grace: NoneOk(8080)Err("PORT non è impostata")Err("invalid digit found in string")Ok(Some(3))Il messaggio «invalid digit found in string» viene dalla libreria standard e resta in inglese: significa che nella stringa c’è un carattere che non è una cifra. transpose si usa di rado, ma è la risposta elegante a «ho un’impostazione facoltativa, e la sua conversione può fallire». Con lui puoi mettere ? sul risultato e tenere l’Option.
Usalo quando un match su un Option ha un ramo interessante e uno noioso.
L’entry API di HashMap
Sezione intitolata “L’entry API di HashMap”Pensa al guardaroba di un teatro. Dici un nome, e l’addetto trova quell’attaccapanni o ne prepara uno nuovo, poi te lo consegna. Un solo viaggio, non due. Questo è .entry(chiave). La pagina sulle HashMap mostra entry(chiave).or_insert(valore), il classico contatore. Vale la pena conoscere altre due varianti:
or_insert_with(f)costruisce il valore predefinito solo quando la chiave manca.or_default()fa lo stesso con il valore predefinito del tipo.and_modify(f)modifica il valore se la chiave esiste; concatenaor_insertdopo di lui per il caso «chiave nuova».
use std::collections::HashMap;
fn main() { let sales = [("mele", 3), ("pere", 2), ("mele", 5), ("prugne", 1), ("pere", 4)];
// and_modify + or_insert: aggiorna se c'è, inserisci se non c'è 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: crea il Vec vuoto solo quando serve 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'ordine di una HashMap è casuale, quindi ordina le chiavi prima di stampare let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}: totale {}, ordini {:?}", totals[fruit], orders[fruit]); }}Output:
mele: totale 8, ordini [3, 5]pere: totale 6, ordini [2, 4]prugne: totale 1, ordini [1]Usalo quando altrimenti scriveresti «se la chiave esiste, modificala, altrimenti inseriscila», che cerca la chiave due volte.
Il never type !: codice che non torna mai indietro
Sezione intitolata “Il never type !: codice che non torna mai indietro”Alcune strade sono a senso unico: una volta entrato, non ne esci più. In Rust un’espressione che non finisce mai ha il never type (il tipo «mai»), scritto !. panic!, return, break, continue, un loop infinito e std::process::exit hanno tutti questo tipo.
Visto che un valore ! non può mai esistere davvero, il compilatore gli permette di prendere il posto di qualsiasi tipo. Ecco perché un match può avere un ramo che dà un i32 e un altro che fa return: il secondo ramo non produce mai un valore, quindi non può rompere i tipi.
use std::process;
// `-> !` significa «questa funzione non ritorna mai»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` ha tipo `!`, quindi va bene dove serve un u32 }; points * 2}
fn main() { println!("facile: {}, difficile: {}, strano: {}", level_points("facile"), level_points("difficile"), level_points("strano"));
let mut total = 0; for input in ["4", "oops", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // anche `continue` è `!` }; total += n; } println!("totale: {total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("nessun file di configurazione trovato, mi fermo"), }; println!("caricato {config}"); // non viene mai stampato}Output:
facile: 20, difficile: 60, strano: 0totale: 10nessun file di configurazione trovato, mi fermoUn avvertimento: process::exit ferma subito il programma, senza eseguire il codice di pulizia (i distruttori) per i valori ancora vivi. Nei programmi reali, ritornare da main di solito è la scelta più gentile.
Due macro usano ! come segnaposto mentre abbozzi il codice. todo!() significa «lo scrivo dopo». unimplemented!() significa «qui non è supportato». Entrambe vanno in panic se vengono raggiunte, ma permettono al resto del programma di compilare:
fn export_pdf(report: &str) -> Vec<u8> { todo!("PDF export comes in v2")}Se la chiami, ottieni not yet implemented: PDF export comes in v2 (il prefisso significa «non ancora implementato»). Con unimplemented!("PDF export") il messaggio è not implemented: PDF export.
Usalo quando una funzione deve fermare il programma, o un ramo deve uscire in anticipo invece di dare un valore.
Piccole comodità
Sezione intitolata “Piccole comodità”Questi sono gli attrezzi da tasca: minuscoli, ma li userai ogni settimana.
matches!(valore, pattern)risponde a «corrisponde al pattern?» contrueofalse(ne parliamo di più nella pagina sul pattern matching).std::cmp::min/maxscelgono il più piccolo o il più grande di due valori, e.clamp(basso, alto)tiene un valore dentro un intervallo, come una manopola del volume che si ferma a 100..abs_diff(altro)dà la distanza tra due numeri. È sempre positiva, e non va mai in overflow, nemmeno con i tipi senza segno.dbg!(espr)stampa il file, la riga, l’espressione e il valore, poi restituisce il valore, così puoi avvolgerlo intorno a qualsiasi cosa.assert!(condizione)va in panic se la condizione è falsa.debug_assert!fa lo stesso, ma solo nelle build di debug, quindi non costa niente nelle build di 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 controllo sì/no su un pattern, con | per «oppure» let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("ordini aperti: {open}");
// min, max e clamp println!("min {}, max {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("volume {volume} limitato a {safe}");
// abs_diff: quanto sono distanti, mai negativo let (guess, secret): (u32, u32) = (30, 42); println!("sei a {} di distanza", guess.abs_diff(secret));
// dbg! stampa su stderr e restituisce il valore let doubled = dbg!(safe * 2);
// assert! ferma il programma se questo diventa falso assert!(doubled <= 200, "volume raddoppiato troppo alto: {doubled}"); debug_assert!(safe <= 100); // controllato solo nelle build di debug println!("tutti i controlli superati");}Output:
ordini aperti: 3min 3, max 7volume 140 limitato a 100sei a 12 di distanzatutti i controlli superatiLa riga di dbg! non compare nell’output qui sopra, perché dbg! scrive su stderr, non su stdout. Nel Playground compare nel pannello «Standard Error»:
[src/main.rs:28:19] safe * 2 = 200È anche per questo che puoi spargere dbg! in giro mentre fai debug: non si mescola con il vero output del tuo programma. Ricordati solo di toglierlo prima di pubblicare.
Anche l’ordinamento ha i suoi piccoli attrezzi. La pagina sull’ordinamento li tratta a fondo; ecco la versione breve. sort_unstable è un po’ più veloce di sort e va bene per i numeri semplici. sort_by_key ordina in base a un campo. binary_search trova un elemento in dati ordinati molto in fretta, come aprire un dizionario a metà invece di leggerlo dalla prima pagina.
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable: un po' più veloce; elementi uguali possono scambiarsi di posto let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search: Ok(indice) se lo trova, Err(indice) dove andrebbe println!("cerca 19: {:?}", ids.binary_search(&19)); println!("cerca 20: {:?}", ids.binary_search(&20));
// sort_by_key: ordina in base a un campo 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!("dal più basso al più alto: {names:?}");}Output:
[3, 7, 19, 25, 42]cerca 19: Ok(2)cerca 20: Err(3)dal più basso al più alto: ["Ben", "Ana", "Chen"]Err(3) non è un fallimento di cui preoccuparsi. Ti dice che 20 non è nella lista, e che l’indice 3 è dove potresti inserirlo per tenere la lista ordinata.
Usalo quando ti accorgi di scrivere un piccolo if o un ciclo che uno di questi fa già.
Errori comuni
Sezione intitolata “Errori comuni”Usare mem::take su un tipo senza Default. mem::take deve lasciare qualcosa al suo posto, quindi ha bisogno di sapere cosa significa «vuoto» per quel tipo:
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ come sarebbe una Connection "vuota"? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ come sarebbe una Connection "vuota"? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`Il compilatore dice che Connection non implementa il trait Default. Risolvi scegliendo tu il sostituto con mem::replace(&mut self.active, Connection { host: "db2".to_string() }), oppure salvando il campo come Option<Connection> e usando self.active.take(). Aggiungi #[derive(Default)] solo se un valore «vuoto» ha davvero senso per quel tipo.
Usare ? su un Option in una funzione che restituisce Result. I due non si mescolano da soli:
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ None non ha un messaggio di errore da restituire 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 non ha un messaggio di errore da restituire | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`Il compilatore ti indica già la soluzione: setting.ok_or("PORT non è impostata".to_string())?, oppure ok_or_else come nell’esempio sopra. Vedi E0277 per le altre forme di questo errore.
Aspettarsi che dedup o binary_search funzionino su dati non ordinati. dedup toglie solo le ripetizioni vicine, e binary_search su dati non ordinati dà una risposta senza senso. Nessuno dei due è un errore di compilazione, quindi è facile non accorgersene. Prima ordina.
Dimenticare gli avanzi di chunks_exact. Un ciclo for su chunks_exact(3) salta in silenzio gli ultimi 1 o 2 elementi. Se ti servono, gestisci .remainder(), oppure usa chunks.
Pratica
Sezione intitolata “Pratica”1. Variazioni di prezzo giornaliere
Sezione intitolata “1. Variazioni di prezzo giornaliere”Hai i prezzi [100, 104, 98, 110, 107]. Costruisci un Vec con la variazione da ogni giorno al successivo, stampalo, poi stampa l’aumento più grande. Suggerimento: windows(2) ti dà ogni coppia come una piccola slice w, quindi la variazione è w[1] - w[0]. .iter().max() trova il più grande.
Mostra la soluzione
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("variazioni: {changes:?}");
let biggest_rise = changes.iter().max(); println!("aumento più grande: {biggest_rise:?}");}Output:
variazioni: [4, -6, 12, -3]aumento più grande: Some(12)Cinque prezzi danno quattro variazioni, perché windows(2) crea una coppia per ogni passo. max() restituisce un Option, visto che una lista vuota non ha un valore più grande.
2. Svuota la coda di stampa
Sezione intitolata “2. Svuota la coda di stampa”Scrivi una struct Printer con un campo queue: Vec<String>. Dagli un metodo add(&mut self, file: &str) e un metodo flush(&mut self) -> Vec<String> che restituisce tutti i file in coda e lascia la coda vuota. Aggiungi due file, svuota la coda e stampa cosa è stato svuotato e quanti file restano. Suggerimento: flush ha solo &mut self, quindi usa std::mem::take.
Mostra la soluzione
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("report.pdf"); printer.add("foto.png");
let jobs = printer.flush(); println!("stampo {} lavori: {jobs:?}", jobs.len()); println!("rimasti in coda: {}", printer.queue.len());}Output:
stampo 2 lavori: ["report.pdf", "foto.png"]rimasti in coda: 0Scrivere solo self.queue in flush fallirebbe con E0507. mem::take sposta fuori il Vec e ne lascia uno vuoto, senza copiare niente.
3. La prima porta valida
Sezione intitolata “3. La prima porta valida”Scrivi una funzione first_port(candidates: &[&str]) -> Option<u16> che restituisce la prima stringa che si converte in un u16. Chiamala con ["", "abc", "70000", "8080", "9090"] e con ["none", "-1"], e stampa la porta, oppure 3000 se non ce n’è nessuna. Suggerimento: find_map con s.parse::<u16>().ok(), poi unwrap_or(3000).
Mostra la soluzione
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 = ["none", "-1"];
println!("uso la porta {}", first_port(&from_config).unwrap_or(3000)); println!("uso la porta {}", first_port(&all_bad).unwrap_or(3000));}Output:
uso la porta 8080uso la porta 3000"70000" sembra un numero, ma non sta in un u16 (il massimo è 65535), quindi la conversione fallisce e find_map va avanti. Si ferma a "8080" e non guarda nemmeno "9090".
Riepilogo
Sezione intitolata “Riepilogo”mem::take,mem::replaceeOption::takespostano un valore fuori da un&mutlasciando qualcosa al suo posto;mem::swapscambia due valori.- Sulle slice,
windowsdà vicini sovrapposti,chunksdà gruppi, esplit_first/split_lastdanno «un elemento e il resto». peekableti permette di guardare avanti;zip,enumerate,chain,fold,find_mapepartitionsostituiscono tanti cicli scritti a mano.- Gli helper di
Optioncomeunwrap_or_default,map_or,filtereok_orti evitano di scrivere piccolimatch, e?funziona anche suOption. entry()conor_insert_withoand_modifyaggiorna unaHashMapcon una sola ricerca.- Il never type
!appartiene al codice che non finisce mai (panic!,return,continue,process::exit), quindi va bene ovunque serva un valore.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Che cosa lascia mem::take(&mut self.items) quando items è un Vec?
2. Che cosa ti dà [1, 2, 3, 4].windows(2)?
3. Perché let x: i32 = match s { "a" => 1, _ => return }; compila?
4. La tua funzione restituisce un Result, ma tu hai un Option. Che cosa aggiungi prima di ?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”std::mem— documentazione della libreria standard (in inglese):take,replace,swape gli altri helper della memoria, con esempi.- Metodi delle slice — documentazione della libreria standard (in inglese): tutti i metodi delle slice, compresi
windows,chunks,split_firstebinary_search. - Il trait
Iterator— documentazione della libreria standard (in inglese): l’elenco completo dei metodi degli iteratori, dazipepeekablefino apartitionefind_map. Option— documentazione della libreria standard (in inglese): tutti gli helper diOption, raggruppati per quello che fanno.- Diverging functions — Rust by Example (in inglese): brevi esempi di funzioni che restituiscono
!e del perché sia utile.