Pattern matching in Rust
Nella pagina Match hai imparato le basi: rami, _, valori raggruppati con | e intervalli. È solo l’inizio. Un pattern in Rust può anche guardare dentro un valore, tirarne fuori le parti che ti servono e controllare condizioni in più, tutto in una riga.
Pensa a un pattern come a una formina per biscotti. La premi su un valore. Se la forma combacia, Rust ti consegna i pezzi che ci sono dentro. Se non combacia, Rust prova il ramo successivo. Quando ti sarà chiaro, userai i pattern dappertutto: nei match, nei let, nei parametri delle funzioni e in if let.
Le guardie: aggiungere un if
Sezione intitolata “Le guardie: aggiungere un if”A volte la forma non basta e ti serve un controllo in più. Una guardia (match guard) è una condizione if scritta dopo il pattern. Il ramo viene eseguito solo se il pattern combacia e la condizione è vera.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "si gela", 0 => "esattamente zero", t if t < 25 => "si sta bene", _ => "fa caldo", }; println!("{t}°C: {feeling}"); }}Output:
-5°C: si gela0°C: esattamente zero18°C: si sta bene35°C: fa caldoRust controlla i rami dall’alto in basso, quindi l’ordine conta. 18 non supera la prima guardia, non è 0, e supera t < 25, quindi «si sta bene».
Destrutturare tuple e struct
Sezione intitolata “Destrutturare tuple e struct”Destrutturare significa scomporre un valore nelle sue parti. Per una tupla, scrivi il pattern con la stessa forma della tupla. Per una struct, indica i campi che ti interessano.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("l'origine"), (x, 0) => println!("sull'asse x in {x}"), (0, y) => println!("sull'asse y in {y}"), (x, y) => println!("da qualche altra parte: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("sull'asse y, y = {y}"), Point { x, y: 0 } => println!("sull'asse x, x = {x}"), Point { x, y } => println!("in ({x}, {y})"), }}Output:
da qualche altra parte: (3, -7)sull'asse y, y = 5Nota il miscuglio. 0 è un valore fisso che deve corrispondere esattamente. x e y sono variabili nuove che catturano qualsiasi cosa ci sia. Point { x, y } è l’abbreviazione di Point { x: x, y: y }.
Destrutturare enum con dati
Sezione intitolata “Destrutturare enum con dati”È qui che i pattern danno il meglio. Una variante di un enum può portare dei dati, e un pattern può spacchettarli mentre controlla quale variante hai.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("area = {:.2}", area(shape)); }}Output:
area = 12.57area = 13.50area = 2.25Non puoi arrivare a radius senza prima dimostrare che il valore è un Circle. È questo il guadagno in sicurezza: il compilatore non ti lascia leggere i dati della variante sbagliata.
Pattern annidati e binding con @
Sezione intitolata “Pattern annidati e binding con @”I pattern possono stare dentro altri pattern, a qualsiasi profondità. Qui un enum Command contiene un enum Direction, e un unico ramo li controlla entrambi.
Un binding con @ ti permette di confrontare un valore con un pattern e di tenerlo in una variabile. n @ 1..=9 significa «controlla che sia tra 1 e 9, e chiamalo n».
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("su di {steps} (passo piccolo)"), Command::Move(Direction::Up, steps) => println!("su di {steps} (gran salto!)"), Command::Move(Direction::Down, steps) => println!("giù di {steps}"), Command::Say(text) => println!("dico: {text}"), Command::Quit => println!("ciao ciao"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("ciao"))); run(Command::Quit);}Output:
su di 3 (passo piccolo)su di 50 (gran salto!)giù di 2dico: ciaociao ciaoSenza @, un intervallo come 1..=9 controlla il valore ma lo butta via. Con @, puoi tenerlo.
Ignorare delle parti con _ e ..
Sezione intitolata “Ignorare delle parti con _ e ..”Spesso ti interessano solo alcuni pezzi. Usa _ per saltare un valore. Usa .. per saltare «tutto il resto».
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("rosso = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("primo = {first}, ultimo = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} è al livello {level}");}Output:
rosso = 255primo = 1, ultimo = 5Mira è al livello 7.. può comparire una sola volta in un pattern. (first, .., last) va bene, ma (.., middle, ..) no: Rust non saprebbe quale valore intendi.
Pattern nei let e nei parametri delle funzioni
Sezione intitolata “Pattern nei let e nei parametri delle funzioni”Ogni let è, sotto sotto, un pattern. Lo è anche ogni parametro di una funzione. Quindi puoi destrutturare proprio nel punto in cui arriva un valore.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Output:
1920x1080x: 4, y: -2[3, 7, 11]La closure |&(a, b)| a + b usa la stessa idea. Ogni elemento è un &(i32, i32), e il pattern lo spacchetta direttamente in a e b. Lo vedrai spesso con gli iteratori.
matches! e let-else
Sezione intitolata “matches! e let-else”Due strumenti comodi ti evitano di scrivere un match completo.
matches!(valore, pattern) restituisce true o false. È perfetto dentro un if o un .filter() quando ti serve solo una risposta sì/no. Anche qui funzionano le guardie.
let-else gestisce il caso «spacchetta questo, oppure esci subito». Scrivi let PATTERN = valore else { ... };. Se il pattern combacia, le variabili sono disponibili per il resto della funzione. Altrimenti viene eseguito il blocco else, che deve uscire: con return, break, continue o un panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' non è un numero"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} promossi");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("trentuno"));}Output:
true3 promossiSome(31)'trentuno' non è un numeroNonelet-else tiene piatto il «percorso felice». Invece di annidare il codice dentro if let ... { }, gestisci prima il caso negativo e vai avanti. Usalo insieme ai valori Option e Result.
Errori comuni
Sezione intitolata “Errori comuni”Le guardie non contano per «tutti i casi coperti». Il compilatore non può ragionare sulle tue condizioni if, quindi questo fallisce anche se le due guardie coprono tutti i numeri:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("negativo"), x if x >= 0 => println!("zero o positivo"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityIl compilatore dice che i pattern non sono esaustivi: nessun ramo copre tutto l’intervallo da i32::MIN a i32::MAX, perché i rami con guardie non contano. La soluzione: rendi l’ultimo ramo un semplice jolly, come _ => println!("zero o positivo").
Un nome di variabile in un pattern è una variabile nuova, non un confronto. Questo sembra controllare guess == expected, ma non lo fa:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Giusto!"), _ => println!("Sbagliato"), }}Compila e stampa Giusto!, perché expected nel ramo crea semplicemente una variabile nuova che cattura qualsiasi valore. Rust ti avvisa:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Giusto!"), | -------- matches any value6 | _ => println!("Sbagliato"), | ^ no value can reach thisL’avviso dice che il pattern _ è irraggiungibile: expected corrisponde a qualsiasi valore, quindi nessun valore arriva al ramo dopo. La soluzione è una guardia: g if g == expected => println!("Giusto!").
Pratica
Sezione intitolata “Pratica”1. FizzBuzz con una tupla
Sezione intitolata “1. FizzBuzz con una tupla”Stampa i numeri da 9 a 15, ma stampa Fizz per i multipli di 3, Buzz per i multipli di 5 e FizzBuzz per i multipli di entrambi. Fallo con un solo match su una tupla invece di una catena di if. Suggerimento: fai il match su (n % 3, n % 5). Un resto di 0 significa «divisibile», e _ significa «non mi interessa».
Mostra la soluzione
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Output:
FizzBuzz11Fizz1314FizzBuzz(0, 0) deve venire per primo. Se ci fosse prima (0, _), il 15 stamperebbe Fizz.
2. Movimenti bancari
Sezione intitolata “2. Movimenti bancari”Definisci enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Fai partire un saldo balance da 100, poi applica in ordine questa lista: deposito di 50, prelievo di 30, prelievo di 500, commissione. Una commissione costa 5. Un prelievo più grande del saldo deve invece essere rifiutato con un messaggio. Stampa il saldo finale. Suggerimento: usa una guardia, Transaction::Withdraw(amount) if amount <= balance, seguita da un secondo ramo Withdraw per il caso rifiutato.
Mostra la soluzione
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("rifiutato: impossibile prelevare {amount}"), Transaction::Fee => balance -= 5, } }
println!("saldo = {balance}");}Output:
rifiutato: impossibile prelevare 500saldo = 115Il secondo ramo Withdraw non ha guardia, quindi cattura ogni prelievo rifiutato dal primo ramo. Senza di esso il match non compilerebbe: le guardie non contano quando Rust controlla che ogni caso sia coperto.
Riepilogo
Sezione intitolata “Riepilogo”- Una guardia (
pattern if condizione) aggiunge un controllo in più a un ramo. - I pattern possono destrutturare tuple, struct e varianti di enum, e si possono annidare.
nome @ patterncontrolla un valore e allo stesso tempo lo tiene in una variabile._salta un valore;..salta il resto. I pattern funzionano anche neilet, nei parametri delle funzioni e nelle closure.matches!dà un rapidotrue/false; let-else spacchetta un valore o esce subito.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa fa n @ 1..=9 in un ramo di un match?
2. In let-else, cosa deve fare il blocco else?
3. Cosa ottieni con let (first, .., last) = (1, 2, 3, 4, 5);?
4. Perché un match con soli rami con guardia spesso non compila?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Patterns and Matching — The Rust Book (in inglese): il capitolo ufficiale su tutti i punti in cui possono comparire i pattern e su quando possono non combaciare.
- Pattern Syntax — The Rust Book (in inglese): un giro tra tutte le forme di pattern, dagli intervalli alla destrutturazione, fino alle guardie e ai binding con
@. - Staying on the Happy Path with let…else — The Rust Book (in inglese): un esempio svolto e graduale di let-else.
- Destructuring — Rust by Example (in inglese): esempi eseguibili di destrutturazione di tuple, array, enum, puntatori e struct.
- La macro
matches!— documentazione di std (in inglese): la sintassi esatta della macro, compreso come aggiungere una guardia.