Match in Rust
L’espressione match è uno degli strumenti più potenti di Rust. Funziona in modo simile a uno switch di altri linguaggi, ma è molto più espressivo grazie al pattern matching. Il compilatore verifica che tutti i possibili casi siano gestiti, garantendo che nessun valore venga trascurato.
Match di base
La sintassi base confronta un valore con una serie di pattern. Ogni ramo (arm) è formato da un pattern, una freccia => e il codice da eseguire.
fn main() {
let numero = 3;
match numero {
1 => println!("Uno"),
2 => println!("Due"),
3 => println!("Tre"),
_ => println!("Altro numero"),
}
}
Il simbolo _ è il pattern catch-all che corrisponde a qualsiasi valore non gestito dai rami precedenti. In Rust, il match deve essere esaustivo: ogni possibile valore deve essere coperto, altrimenti il codice non compila.
Pattern multipli con |
È possibile combinare più pattern in un singolo ramo usando l’operatore | (pipe).
fn main() {
let giorno = "sabato";
match giorno {
"sabato" | "domenica" => println!("Weekend!"),
"lunedi" | "martedi" | "mercoledi" | "giovedi" | "venerdi" => {
println!("Giorno lavorativo")
}
_ => println!("Giorno non valido"),
}
}
Pattern con range
I range permettono di verificare se un valore cade all’interno di un intervallo. Si usa la sintassi ..= per range inclusivi.
fn main() {
let voto = 85;
let giudizio = match voto {
90..=100 => "Eccellente",
80..=89 => "Buono",
60..=79 => "Sufficiente",
0..=59 => "Insufficiente",
_ => "Voto non valido",
};
println!("Giudizio: {giudizio}");
}
I range funzionano con interi e caratteri:
fn main() {
let lettera = 'c';
match lettera {
'a'..='f' => println!("Prima metà dell'alfabeto (a-f)"),
'g'..='z' => println!("Seconda metà dell'alfabeto (g-z)"),
_ => println!("Non è una lettera minuscola"),
}
}
Catch-all con _
Il pattern _ corrisponde a qualsiasi valore senza vincolarlo a una variabile. È usato come ultimo ramo per gestire tutti i casi rimanenti.
fn main() {
let codice = 404;
match codice {
200 => println!("OK"),
404 => println!("Non trovato"),
500 => println!("Errore del server"),
_ => println!("Codice sconosciuto: {codice}"),
}
}
Se non serve il catch-all ma si vuole catturare il valore, si può usare una variabile:
fn main() {
let numero = 42;
match numero {
1 => println!("Uno"),
2 => println!("Due"),
altro => println!("Altro valore: {altro}"),
}
}
Match come espressione
Come l’if, anche match è un’espressione e può restituire un valore.
fn main() {
let dado = 4;
let risultato = match dado {
1 => "Uno",
2 => "Due",
3 => "Tre",
4 => "Quattro",
5 => "Cinque",
6 => "Sei",
_ => "Valore non valido",
};
println!("Hai tirato: {risultato}");
}
Tutti i rami devono restituire lo stesso tipo, altrimenti il compilatore segnalerà un errore.
Match con enum
Il match è particolarmente utile con le enum, dove il compilatore verifica che tutti i varianti siano gestiti.
enum Direzione {
Nord,
Sud,
Est,
Ovest,
}
fn descrivi_direzione(dir: Direzione) {
match dir {
Direzione::Nord => println!("Vai verso nord"),
Direzione::Sud => println!("Vai verso sud"),
Direzione::Est => println!("Vai verso est"),
Direzione::Ovest => println!("Vai verso ovest"),
}
}
fn main() {
descrivi_direzione(Direzione::Nord);
}
Con enum che contengono dati, è possibile destrutturare i valori interni:
enum Messaggio {
Testo(String),
Numero(i32),
Coordinate { x: f64, y: f64 },
}
fn gestisci_messaggio(msg: Messaggio) {
match msg {
Messaggio::Testo(t) => println!("Testo: {t}"),
Messaggio::Numero(n) => println!("Numero: {n}"),
Messaggio::Coordinate { x, y } => println!("Posizione: ({x}, {y})"),
}
}
fn main() {
gestisci_messaggio(Messaggio::Testo(String::from("Ciao")));
gestisci_messaggio(Messaggio::Coordinate { x: 3.5, y: 7.2 });
}
Match con tuple e struct
Il pattern matching funziona anche con tuple e strutture.
fn main() {
let punto = (0, -2);
match punto {
(0, 0) => println!("Origine"),
(x, 0) => println!("Sull'asse X, x = {x}"),
(0, y) => println!("Sull'asse Y, y = {y}"),
(x, y) => println!("Punto generico: ({x}, {y})"),
}
}
Con le struct, si usa la destrutturazione:
struct Punto {
x: i32,
y: i32,
}
fn main() {
let p = Punto { x: 0, y: 5 };
match p {
Punto { x: 0, y: 0 } => println!("Origine"),
Punto { x, y: 0 } => println!("Asse X: x = {x}"),
Punto { x: 0, y } => println!("Asse Y: y = {y}"),
Punto { x, y } => println!("Punto: ({x}, {y})"),
}
}
Match guard
Le match guard aggiungono una condizione extra a un pattern tramite la parola chiave if. Questo permette di esprimere logiche più complesse.
fn main() {
let numero = 4;
match numero {
n if n < 0 => println!("{n} è negativo"),
n if n == 0 => println!("È zero"),
n if n % 2 == 0 => println!("{n} è pari e positivo"),
n => println!("{n} è dispari e positivo"),
}
}
Le guard sono utili quando il pattern da solo non è sufficiente a esprimere la condizione desiderata:
fn main() {
let temperatura = Some(38);
match temperatura {
Some(t) if t > 37 => println!("Febbre! Temperatura: {t}°C"),
Some(t) => println!("Temperatura normale: {t}°C"),
None => println!("Temperatura non rilevata"),
}
}
Conclusione
L’espressione match è uno strumento fondamentale in Rust. Grazie alla sua esaustività controllata dal compilatore, al supporto per pattern complessi e alle match guard, permette di scrivere codice robusto e leggibile. Usa match ogni volta che hai bisogno di gestire più casi distinti, specialmente quando lavori con enum e tipi composti.