Esercizi di Rust
Leggere di Rust è utile. Ma è scrivendo Rust che le cose ti restano in testa. Questa pagina contiene 18 piccoli problemi, ordinati dal più facile al più difficile e raggruppati per argomento.
Per ognuno leggi il compito, prova prima da solo e solo dopo apri la soluzione. È del tutto normale bloccarsi o vedere errori del compilatore. Fa parte dell’imparare, e i messaggi di errore di Rust sono spesso il miglior suggerimento che riceverai.
Come esercitarti: apri il Rust Playground in un’altra scheda, oppure crea un progetto con cargo new practice (vedi Primi passi). Scrivi lì la tua risposta. Poi confrontala con la soluzione. Se la tua risposta è diversa ma stampa la stessa cosa, probabilmente va bene. C’è quasi sempre più di un modo giusto.
1. FizzBuzz
Sezione intitolata “1. FizzBuzz”Stampa i numeri da 1 a 15. Però per i multipli di 3 stampa Fizz, per i multipli di 5 stampa Buzz e per i multipli di entrambi stampa FizzBuzz.
Suggerimento: usa un ciclo for su 1..=15 e l’operatore % (resto). Controlla prima il caso «multiplo di entrambi».
Mostra la soluzione
fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } }}Output:
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzzL’ordine conta. Se controlli prima n % 3 == 0, allora 15 stampa Fizz e non arriva mai al ramo FizzBuzz. Un numero divisibile sia per 3 sia per 5 è divisibile per 15.
2. Invertire una stringa
Sezione intitolata “2. Invertire una stringa”Scrivi una funzione reverse(s: &str) -> String che restituisce il testo al contrario. "roma" deve diventare "amor".
Suggerimento: .chars() ti dà i caratteri uno alla volta, .rev() inverte un iteratore e .collect() può costruire una String.
Mostra la soluzione
fn reverse(s: &str) -> String { s.chars().rev().collect()}
fn main() { println!("{}", reverse("roma")); println!("{}", reverse("ciao")); println!("{}", reverse("però"));}Output:
amoroaicòrepPerché .chars() e non i byte? Una String di Rust è in UTF-8, quindi un carattere come ò occupa due byte. Invertire i byte lo spezzerebbe e il testo non sarebbe più valido. .chars() lavora su caratteri interi. Trovi di più nella pagina Stringhe.
3. Contare le vocali
Sezione intitolata “3. Contare le vocali”Scrivi count_vowels(s: &str) -> usize che conta le vocali (a, e, i, o, u) in una stringa. Contano sia le minuscole sia le maiuscole.
Suggerimento: .chars().filter(...).count(). Una stringa come "aeiouAEIOU" ha un metodo .contains(c) che puoi usare per controllare ogni carattere.
Mostra la soluzione
fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
fn main() { println!("{}", count_vowels("Ciao, mondo")); println!("{}", count_vowels("Rust mi piace")); println!("{}", count_vowels("AEIOU e a volte y")); println!("{}", count_vowels("psst"));}Output:
5590filter passa alla closure un riferimento a ogni elemento (&char), quindi scriviamo *c per ottenere il char vero e proprio. Le pagine Closure e Iteratori spiegano questo schema. Nota che le vocali accentate come è non sono nella lista "aeiouAEIOU", quindi non verrebbero contate: se ti servono, aggiungile alla stringa.
Ownership e borrowing
Sezione intitolata “Ownership e borrowing”4. Correggi l’errore di spostamento
Sezione intitolata “4. Correggi l’errore di spostamento”Questo programma non compila. Cambia solo il parametro della funzione shout e le due chiamate, in modo che stampi HELLO! due volte.
fn shout(text: String) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hello"); shout(word); shout(word);}error[E0382]: use of moved value: `word` --> src/main.rs:8:11 |6 | let word = String::from("hello"); | ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait7 | shout(word); | ---- value moved here8 | shout(word); | ^^^^ value used here after moveIl compilatore dice che word viene usato dopo essere stato spostato (move): la prima chiamata a shout l’ha già preso. Il messaggio completo è spiegato nella pagina E0382.
Suggerimento: shout deve solo guardare il testo, non possederlo. Vedi Ownership.
Mostra la soluzione
fn shout(text: &str) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hello"); shout(&word); shout(&word);}Output:
HELLO!HELLO!Quando una funzione riceve una String, chiamarla sposta dentro la stringa, e main non può più usarla. Ricevere un &str significa che la funzione la prende solo in prestito. Il compilatore suggerisce anche word.clone(), che funziona, ma copia l’intera stringa senza motivo. Qui la soluzione migliore è il prestito.
5. Correggi l’errore di prestito
Sezione intitolata “5. Correggi l’errore di prestito”Anche questo programma non compila. Fai in modo che stampi Ana, aggiungendo comunque Ben alla lista.
fn main() { let mut names = vec![String::from("Ana")]; let first = &names[0]; names.push(String::from("Ben")); println!("{first}");}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Ben")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereIl compilatore dice che non puoi prendere in prestito names come mutabile, perché è già in prestito come immutabile (tramite first), e quel prestito viene usato più avanti. Vedi anche E0502.
Suggerimento: il problema è l’ordine. first è ancora in uso quando push modifica il vettore. Vedi Prestito (borrowing).
Mostra la soluzione
fn main() { let mut names = vec![String::from("Ana")]; names.push(String::from("Ben")); let first = &names[0]; println!("{first}");}Output:
AnaPerché Rust è così severo qui? push potrebbe aver bisogno di più spazio, e allora il vettore può spostare tutti i suoi elementi in una zona di memoria nuova e più grande. A quel punto first punterebbe alla vecchia zona, che non è più valida. La regola di Rust (tanti lettori oppure un solo scrittore, mai entrambi insieme) blocca questo bug prima ancora che il programma parta. Prendere il riferimento dopo il push risolve il problema.
6. La prima parola di una stringa
Sezione intitolata “6. La prima parola di una stringa”Scrivi first_word(s: &str) -> &str che restituisce la prima parola (tutto quello che c’è prima del primo spazio). Se non c’è nessuno spazio, restituisci l’intera stringa. Non creare una nuova String: restituisci una slice dell’originale.
Suggerimento: s.find(' ') restituisce un Option<usize> con la posizione del primo spazio. &s[..i] è una slice dall’inizio fino a i.
Mostra la soluzione
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { println!("{:?}", first_word("ciao mondo")); println!("{:?}", first_word("Rust")); println!("{:?}", first_word(""));}Output:
"ciao""Rust"""Il &str restituito punta alla stessa memoria dell’input, quindi non viene copiato niente. Il compilatore sa che il risultato non può vivere più a lungo dell’input. È l’elisione dei lifetime al lavoro: con un riferimento in entrata e uno in uscita, non devi scrivere i lifetime a mano.
Una versione più corta è s.split_whitespace().next().unwrap_or(""), che salta anche gli spazi in più all’inizio.
Collezioni
Sezione intitolata “Collezioni”7. Il numero più grande in un vettore
Sezione intitolata “7. Il numero più grande in un vettore”Scrivi largest(numbers: &[i32]) -> Option<i32>. Restituisce il numero più grande, oppure None se la lista è vuota. Questa volta non usare il metodo .max() già pronto: scrivi tu il ciclo.
Suggerimento: parti dal primo elemento come «il più grande finora». numbers.first() restituisce un Option, quindi gestisce per te il caso della lista vuota.
Mostra la soluzione
fn largest(numbers: &[i32]) -> Option<i32> { let mut biggest = *numbers.first()?; for &n in numbers { if n > biggest { biggest = n; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[-5, -2, -9])); println!("{:?}", largest(&[]));}Output:
Some(41)Some(-2)Nonenumbers.first()? restituisce subito None se la lista è vuota. Ecco perché la funzione restituisce un Option: «il più grande del nulla» non ha una risposta sensata, e Rust ti obbliga a dirlo. Nel codice vero, numbers.iter().max() fa la stessa cosa.
Partire da 0 invece che dal primo elemento è un bug classico: la lista [-5, -2, -9] darebbe 0, che è sbagliato.
8. Frequenza delle parole
Sezione intitolata “8. Frequenza delle parole”Conta quante volte compare ogni parola in una frase. Ignora maiuscole, minuscole e punteggiatura, e stampa le parole in ordine alfabetico.
Suggerimento: usa una BTreeMap<String, usize>. Funziona come una HashMap, ma tiene le chiavi in ordine. map.entry(word).or_insert(0) ti dà un contatore a cui aggiungere 1.
Mostra la soluzione
use std::collections::BTreeMap;
fn main() { let text = "Il gatto e il topo. Il gatto dorme!"; let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() { let clean = word .trim_matches(|c: char| !c.is_alphanumeric()) .to_lowercase(); if clean.is_empty() { continue; } *counts.entry(clean).or_insert(0) += 1; }
for (word, count) in &counts { println!("{word}: {count}"); }}Output:
dorme: 1e: 1gatto: 2il: 3topo: 1Perché BTreeMap e non HashMap? Una HashMap ti restituisce gli elementi in un ordine imprevedibile, che può cambiare da un’esecuzione all’altra. Se ti serve un output ordinato, usa una BTreeMap, oppure raccogli la HashMap in un Vec e ordinalo.
trim_matches toglie il . da topo. e il ! da dorme!, e to_lowercase fa diventare Il e il la stessa parola. Attenzione: questo ordine alfabetico confronta i codici dei caratteri, quindi le parole con lettere accentate (come è) finirebbero dopo la z.
9. I quadrati dei numeri pari
Sezione intitolata “9. I quadrati dei numeri pari”Dato vec![1, 2, 3, 4, 5, 6, 7, 8], costruisci un nuovo vettore con i quadrati dei soli numeri pari: [4, 16, 36, 64]. Poi stampa la loro somma. Usa i metodi degli iteratori, non un ciclo for.
Suggerimento: concatena .iter(), .filter(...), .map(...) e .collect(). Poi .iter().sum() li somma.
Mostra la soluzione
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares); println!("Somma: {total}");}Output:
[4, 16, 36, 64]Somma: 120Leggi la catena dall’alto in basso come una ricetta: prendi i numeri, tieni quelli pari, eleva ognuno al quadrato, mettili in un Vec. Gli iteratori sono pigri (lazy), quindi non succede niente finché .collect() non chiede i risultati. Vedi Iteratori.
Gestione degli errori
Sezione intitolata “Gestione degli errori”10. Leggere e sommare numeri
Sezione intitolata “10. Leggere e sommare numeri”Scrivi sum_numbers(input: &str) -> Result<i32, ParseIntError>. Riceve un testo come "1, 2, 3", converte ogni numero e restituisce la somma. Se un pezzo non è un numero, restituisci l’errore invece di far crashare il programma.
Suggerimento: dividi su ',', applica .trim() a ogni pezzo, poi .parse(). Usa l’operatore ? per uscire subito in caso di errore.
Mostra la soluzione
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> { let mut total = 0; for piece in input.split(',') { let n: i32 = piece.trim().parse()?; total += n; } Ok(total)}
fn main() { for input in ["1, 2, 3", "10,20, 30", "4, cinque, 6", ""] { match sum_numbers(input) { Ok(sum) => println!("{input:?} -> la somma è {sum}"), Err(e) => println!("{input:?} -> errore: {e}"), } }}Output:
"1, 2, 3" -> la somma è 6"10,20, 30" -> la somma è 60"4, cinque, 6" -> errore: invalid digit found in string"" -> errore: cannot parse integer from empty stringI messaggi di errore vengono dalla libreria standard e sono in inglese: «invalid digit found in string» significa «trovata una cifra non valida nella stringa», e «cannot parse integer from empty string» significa «impossibile leggere un intero da una stringa vuota».
? fa due cose con un solo carattere: se parse() dà Ok(n), ottieni n. Se dà Err(e), la funzione restituisce subito Err(e). Ecco perché anche la funzione deve restituire un Result.
Bonus: Result funziona con .sum(), quindi questa riga sola fa la stessa cosa e si ferma al primo errore:
input.split(',').map(|p| p.trim().parse::<i32>()).sum()11. Concatenare Option
Sezione intitolata “11. Concatenare Option”Ogni utente può avere oppure no un’email. Scrivi email_domain(user: &User) -> Option<&str> che restituisce la parte dopo la @. Restituisci None se non c’è un’email, o se l’email non contiene una @. Poi cerca gli utenti per nome e stampa il loro dominio.
Suggerimento: user.email.as_deref() trasforma un Option<String> in un Option<&str>. email.split_once('@') restituisce un Option<(&str, &str)>. L’operatore ? funziona anche con Option. Vedi Option.
Mostra la soluzione
struct User { name: String, email: Option<String>,}
fn email_domain(user: &User) -> Option<&str> { let email = user.email.as_deref()?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { let users = vec![ User { name: String::from("Ben"), email: None }, User { name: String::from("Cleo"), email: Some(String::from("cleo-a-casa")) }, ];
for name in ["Ana", "Ben", "Cleo", "Dan"] { let domain = users .iter() .find(|u| u.name == name) .and_then(email_domain) .unwrap_or("(nessun dominio)"); println!("{name}: {domain}"); }}Output:
Ana: example.comBen: (nessun dominio)Cleo: (nessun dominio)Dan: (nessun dominio)Qui ci sono tre modi diversi di «fallire»: Dan non esiste, Ben non ha un’email e l’email di Cleo non ha la @. Ognuno è un passaggio con Option, e ? o .and_then() si fermano al primo None. Niente controlli con if, niente errori da puntatore nullo. .unwrap_or(...) fornisce un valore predefinito proprio alla fine.
12. Un conto in banca
Sezione intitolata “12. Un conto in banca”Crea una struct Account con un titolare e un saldo. Dalle i metodi deposit(amount) e withdraw(amount). withdraw deve restituire Result<u64, String>: il nuovo saldo se l’operazione riesce, oppure un messaggio di errore se i soldi non bastano. Il saldo non deve mai scendere sotto zero.
Suggerimento: i metodi che modificano il conto ricevono &mut self. Controlla il saldo prima di sottrarre. Vedi Struct e Result.
Mostra la soluzione
struct Account { owner: String, balance: u64,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }
fn deposit(&mut self, amount: u64) { self.balance += amount; }
fn withdraw(&mut self, amount: u64) -> Result<u64, String> { if amount > self.balance { return Err(format!( "impossibile prelevare {amount}: il saldo è solo {}", self.balance )); } self.balance -= amount; Ok(self.balance) }}
fn main() { let mut account = Account::new("Ana"); account.deposit(100);
for amount in [30, 50, 40] { match account.withdraw(amount) { Ok(left) => println!("Prelevati {amount}, ne restano {left}"), Err(e) => println!("Errore: {e}"), } }
println!("Alla fine {} ha {}", account.owner, account.balance);}Output:
Prelevati 30, ne restano 70Prelevati 50, ne restano 20Errore: impossibile prelevare 40: il saldo è solo 20Alla fine Ana ha 20Usare u64 (un numero senza segno) è una bella rete di sicurezza in più: non può proprio contenere un saldo negativo. Se ti dimenticassi il controllo, in modalità debug self.balance -= amount andrebbe in panic con «attempt to subtract with overflow» (tentativo di sottrazione con overflow), invece di diventare negativo in silenzio.
Trait e generici
Sezione intitolata “Trait e generici”13. Un trait Shape
Sezione intitolata “13. Un trait Shape”Definisci un trait Shape con due metodi: area(&self) -> f64 e name(&self) -> String. Implementalo per un Circle (con un raggio) e un Rectangle (con larghezza e altezza). Metti entrambi in un unico Vec e stampa il nome e l’area di ognuno con 2 cifre decimali.
Suggerimento: per memorizzare tipi diversi in un solo Vec, usa Vec<Box<dyn Shape>>. Vedi Trait e Trait object. {:.2} stampa 2 decimali.
Mostra la soluzione
trait Shape { fn area(&self) -> f64; fn name(&self) -> String;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> String { format!("Cerchio (r = {})", self.radius) }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> String { format!("Rettangolo ({} x {})", self.width, self.height) }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 3.0, height: 4.0 }), Box::new(Circle { radius: 2.5 }), ];
let mut total = 0.0; for shape in &shapes { println!("{}: {:.2}", shape.name(), shape.area()); total += shape.area(); } println!("Area totale: {total:.2}");}Output:
Cerchio (r = 1): 3.14Rettangolo (3 x 4): 12.00Cerchio (r = 2.5): 19.63Area totale: 34.78Al ciclo non importa quale forma ha in mano, e non lo sa nemmeno. Sa solo che ogni elemento «è uno Shape», quindi può chiamare area() e name(). Aggiungere un Triangle più avanti significa scrivere un solo nuovo impl, e il ciclo non cambia affatto.
14. Un largest generico
Sezione intitolata “14. Un largest generico”Torna all’esercizio 7, ma fallo funzionare per qualsiasi tipo che si possa confrontare: interi, decimali, caratteri e stringhe. La firma è largest<T: PartialOrd>(items: &[T]) -> Option<&T>.
Suggerimento: T: PartialOrd significa «T supporta < e >». Restituisci un riferimento (&T), così non devi copiare o clonare gli elementi. Vedi Generici.
Mostra la soluzione
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> { let mut biggest = items.first()?; for item in items { if item > biggest { biggest = item; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[2.5, -1.0, 9.75])); println!("{:?}", largest(&['r', 'u', 's', 't'])); println!("{:?}", largest(&["pera", "mela", "zucchina"]));
let empty: Vec<i32> = Vec::new(); println!("{:?}", largest(&empty));}Output:
Some(41)Some(9.75)Some('u')Some("zucchina")NoneIl corpo è quasi identico a quello dell’esercizio 7. L’unica vera differenza è la parte <T: PartialOrd>. Senza quel vincolo, il compilatore rifiuta item > biggest, perché non tutti i tipi si possono confrontare. Le stringhe si confrontano in ordine alfabetico, ed è per questo che vince "zucchina".
Nota il Vec<i32> sul vettore vuoto: senza elementi, Rust non può indovinare che cosa sia T, quindi devi dirglielo tu.
Insiemi, ordinamento, pattern e iteratori
Sezione intitolata “Insiemi, ordinamento, pattern e iteratori”15. Trova i doppioni
Sezione intitolata “15. Trova i doppioni”In una lista di invitati alcuni nomi sono scritti più di una volta. Scrivi find_repeats(words: &[&str]) -> Vec<String> che restituisce ogni nome che compare più di una volta, elencato una volta sola, in ordine alfabetico. Stampa anche quanti invitati diversi ci sono.
Suggerimento: un HashSet ricorda quello che hai già visto. Il suo metodo insert restituisce false quando il valore c’era già. Un HashSet non ha un ordine fisso, quindi ordina il risultato alla fine.
Mostra la soluzione
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> { let mut seen = HashSet::new(); let mut repeats = HashSet::new();
for &word in words { // insert restituisce false se la parola era già nell'insieme if !seen.insert(word) { repeats.insert(word); } }
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect(); result.sort(); result}
fn main() { let guests = ["ana", "ben", "cleo", "ben", "dan", "ana", "ben"]; println!("Doppioni: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect(); println!("Invitati diversi: {}", unique.len());
println!("Doppioni: {:?}", find_repeats(&["x", "y", "z"]));}Output:
Doppioni: ["ana", "ben"]Invitati diversi: 4Doppioni: []Perché due insiemi? seen contiene tutti i nomi visti finora. repeats contiene i nomi che abbiamo incontrato una seconda volta. "ben" compare tre volte, ma siccome repeats è un insieme, viene memorizzato una volta sola.
Raccogliere in un HashSet è anche il modo più rapido per contare i valori unici: i duplicati semplicemente spariscono. Senza la riga result.sort(), l’ordine dei doppioni potrebbe cambiare da un’esecuzione all’altra.
16. Ordinare le parole per lunghezza
Sezione intitolata “16. Ordinare le parole per lunghezza”Ordina una lista di parole dalla più corta alla più lunga. Quando due parole hanno la stessa lunghezza, mettile in ordine dalla A alla Z, ignorando maiuscole e minuscole. Poi confronta il risultato con un semplice .sort().
Suggerimento: sort_by riceve una closure che confronta due elementi e restituisce un Ordering. .then_with(...) aggiunge un secondo confronto, che conta solo in caso di parità. Vedi Ordinamento e ricerca.
Mostra la soluzione
fn main() { let mut words = vec!["pera", "Fico", "banana", "mela", "kiwi", "Prugna", "dattero"];
let mut plain = words.clone(); plain.sort(); println!("Ordine semplice: {plain:?}");
words.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| a.to_lowercase().cmp(&b.to_lowercase())) }); println!("Per lunghezza: {words:?}");}Output:
Ordine semplice: ["Fico", "Prugna", "banana", "dattero", "kiwi", "mela", "pera"]Per lunghezza: ["Fico", "kiwi", "mela", "pera", "banana", "Prugna", "dattero"]Guarda l’ordine semplice: "Fico" e "Prugna" vengono per prime. Le stringhe si confrontano in base ai codici dei caratteri, e ogni lettera maiuscola viene prima di ogni lettera minuscola. Ecco perché nello spareggio confrontiamo le versioni to_lowercase(): così "banana" passa davanti a "Prugna", che ha la stessa lunghezza.
Un modo più corto di scrivere la stessa regola è words.sort_by_key(|w| (w.len(), w.to_lowercase())). Le tuple si confrontano campo per campo, quindi decide prima la lunghezza e la parola in minuscolo risolve le parità.
17. Costi di spedizione con le guardie del match
Sezione intitolata “17. Costi di spedizione con le guardie del match”Scrivi shipping_cost(p: Package) -> String per questo enum, usando un solo match:
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}Le regole: il ritiro in negozio è gratis. Le lettere fino a 20 g costano 1 €, quelle più pesanti 3 €. I pacchi sopra i 30 kg non si possono spedire affatto. I pacchi espressi costano 10 € più 2 € al kg. Gli altri pacchi costano 5 € più 1 € al kg.
Suggerimento: un ramo del match può avere un if dopo il pattern, chiamato guardia (guard): Package::Letter { grams } if grams <= 20 => .... I rami vengono controllati dall’alto in basso, quindi metti la regola «troppo pesante» prima di quella dell’espresso. Vedi Pattern matching.
Mostra la soluzione
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}
fn shipping_cost(p: Package) -> String { match p { Package::Pickup => String::from("gratis (ritiro in negozio)"), Package::Letter { grams } if grams <= 20 => String::from("1 € (lettera normale)"), Package::Letter { grams } => format!("3 € (lettera grande, {grams} g)"), Package::Parcel { kg, .. } if kg > 30 => format!("troppo pesante da spedire ({kg} kg)"), Package::Parcel { kg, express: true } => format!("{} € (espresso)", 10 + kg * 2), Package::Parcel { kg, express: false } => format!("{} € (pacco normale)", 5 + kg), }}
fn main() { let packages = vec![ Package::Letter { grams: 15 }, Package::Letter { grams: 80 }, Package::Parcel { kg: 4, express: false }, Package::Parcel { kg: 4, express: true }, Package::Parcel { kg: 35, express: true }, Package::Pickup, ];
for p in packages { println!("{}", shipping_cost(p)); }}Output:
1 € (lettera normale)3 € (lettera grande, 80 g)9 € (pacco normale)18 € (espresso)troppo pesante da spedire (35 kg)gratis (ritiro in negozio)Il caso interessante è il pacco espresso da 35 kg. Corrisponde a due rami, ma vince solo il primo. Se mettessi il ramo dell’espresso sopra quello del «troppo pesante», il programma chiederebbe allegramente 80 € per un pacco che non puoi spedire.
Nota che l’ultimo ramo dice express: false invece di ... Non è obbligatorio, ma così il ramo dice esattamente che cosa gestisce. Il compilatore controlla comunque che ogni caso sia coperto. Le guardie non contano per quel controllo, ed è per questo che serve il ramo Package::Letter { grams } senza guardia.
18. Ripulire un report di magazzino
Sezione intitolata “18. Ripulire un report di magazzino”Un report di magazzino ha un articolo per riga, come "mele 12". Alcune righe sono rovinate: vuote, oppure con una quantità che non è un numero. Usando i metodi degli iteratori (niente ciclo for), tieni solo le righe che si possono leggere e che hanno una quantità maggiore di zero. Stampale come nome: quantità separate da virgole, poi stampa il totale.
Suggerimento: .lines() divide il testo in righe. filter_map tiene solo i valori Some restituiti dalla sua closure, quindi può filtrare e convertire in un solo passaggio. .ok() trasforma un Result in un Option. Vedi Iteratori.
Mostra la soluzione
fn main() { let report = "mele 12\nbanane x\npere 0\n\nciliegie 40\nprugne 7";
let in_stock: Vec<(&str, u32)> = report .lines() .filter_map(|line| line.split_once(' ')) .filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n))) .filter(|&(_, n)| n > 0) .collect();
let list: Vec<String> = in_stock .iter() .map(|(name, n)| format!("{name}: {n}")) .collect(); let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", ")); println!("Articoli in totale: {total}");}Output:
mele: 12, ciliegie: 40, prugne: 7Articoli in totale: 59Segui una riga alla volta. La riga vuota non ha spazi, quindi split_once dà None e il primo filter_map la scarta. "banane x" si divide senza problemi, ma "x" non è un numero, quindi la scarta il secondo filter_map. "pere 0" supera entrambi, e poi filter la toglie perché la quantità è zero.
Facciamo collect in un Vec una volta sola, poi lo percorriamo due volte: una per costruire il testo, una per il totale. Un iteratore si può usare una volta sola, quindi conservare i dati ripuliti è più semplice che leggere tutto due volte.
Dove andare adesso
Sezione intitolata “Dove andare adesso”Se li hai finiti tutti e 18, complimenti. È un vero passo avanti. 🦀
- Costruisci un programma completo: prova il progetto Gioco «indovina il numero», il Contatore di parole o la Lista di cose da fare da riga di comando.
- Ti stai preparando per un colloquio di lavoro? Leggi le Domande da colloquio su Rust.
- Vuoi fare ancora più pratica? I link qui sotto hanno tanti altri problemi.
Per approfondire
Sezione intitolata “Per approfondire”- Rustlings (in inglese): la raccolta ufficiale di piccoli programmi rotti da correggere uno alla volta nel terminale. Un ottimo passo successivo dopo questa pagina.
- Exercism, percorso Rust (in inglese): circa 100 problemi con i test, e commenti gratuiti sulle tue soluzioni da parte di mentori in carne e ossa.
- Rust by Example (in inglese): esempi brevi ed eseguibili per ogni argomento. Comodo quando sei bloccato sulla sintassi di un esercizio.
- Il trait
Iterator— documentazione std (in inglese): tutti i metodi degli iteratori (filter_map,fold,zipe molti altri), ognuno con un piccolo esempio. HashSet— documentazione std (in inglese): tutti i metodi degli insiemi, compresiunion,intersectionedifference.