Salta ai contenuti

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.

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:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Provalo tu

L’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.

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:

amor
oaic
òrep
▶ Provalo tu

Perché .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.

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:

5
5
9
0
▶ Provalo tu

filter 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.

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` trait
7 | shout(word);
| ---- value moved here
8 | shout(word);
| ^^^^ value used here after move

Il 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!
▶ Provalo tu

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.

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 here
4 | names.push(String::from("Ben"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here

Il 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:

Ana
▶ Provalo tu

Perché 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.

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"
""
▶ Provalo tu

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.

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)
None
▶ Provalo tu

numbers.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.

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: 1
e: 1
gatto: 2
il: 3
topo: 1
▶ Provalo tu

Perché 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.

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: 120
▶ Provalo tu

Leggi 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.

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 string
▶ Provalo tu

I 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()

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("Ana"), email: Some(String::from("[email protected]")) },
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.com
Ben: (nessun dominio)
Cleo: (nessun dominio)
Dan: (nessun dominio)
▶ Provalo tu

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.

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 70
Prelevati 50, ne restano 20
Errore: impossibile prelevare 40: il saldo è solo 20
Alla fine Ana ha 20
▶ Provalo tu

Usare 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.

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.14
Rettangolo (3 x 4): 12.00
Cerchio (r = 2.5): 19.63
Area totale: 34.78
▶ Provalo tu

Al 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.

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")
None
▶ Provalo tu

Il 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.

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: 4
Doppioni: []
▶ Provalo tu

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.

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"]
▶ Provalo tu

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à.

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)
▶ Provalo tu

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.

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: 7
Articoli in totale: 59
▶ Provalo tu

Segui 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.

Se li hai finiti tutti e 18, complimenti. È un vero passo avanti. 🦀

  • 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, zip e molti altri), ognuno con un piccolo esempio.
  • HashSet — documentazione std (in inglese): tutti i metodi degli insiemi, compresi union, intersection e difference.