Pattern Matching Avanzato in Rust
Il pattern matching e una delle funzionalita piu potenti e pervasive di Rust. Va ben oltre il semplice match su enum: permette di destrutturare dati complessi, vincolarne i valori e prendere decisioni in modo espressivo e sicuro.
Destrutturazione di Struct
Si possono estrarre i campi di una struct direttamente nel pattern:
struct Punto {
x: f64,
y: f64,
}
fn descrivi_punto(p: &Punto) {
match p {
Punto { x: 0.0, y: 0.0 } => println!("Origine"),
Punto { x, y: 0.0 } => println!("Sull'asse X a {}", x),
Punto { x: 0.0, y } => println!("Sull'asse Y a {}", y),
Punto { x, y } => println!("Punto ({}, {})", x, y),
}
}
fn main() {
descrivi_punto(&Punto { x: 0.0, y: 0.0 });
descrivi_punto(&Punto { x: 3.0, y: 0.0 });
descrivi_punto(&Punto { x: 0.0, y: 5.0 });
descrivi_punto(&Punto { x: 3.0, y: 4.0 });
}
Si puo usare .. per ignorare i campi rimanenti:
struct Configurazione {
host: String,
porta: u16,
debug: bool,
max_connessioni: u32,
}
fn stampa_host(config: &Configurazione) {
let Configurazione { host, porta, .. } = config;
println!("Server: {}:{}", host, porta);
}
Destrutturazione di Enum
La destrutturazione degli enum e fondamentale per accedere ai dati delle varianti:
enum Evento {
Click { x: i32, y: i32, bottone: String },
Tasto(char),
Ridimensiona(u32, u32),
Chiudi,
}
fn gestisci_evento(evento: &Evento) {
match evento {
Evento::Click { x, y, bottone } => {
println!("Click {} a ({}, {})", bottone, x, y);
}
Evento::Tasto(c) => println!("Premuto tasto: {}", c),
Evento::Ridimensiona(larghezza, altezza) => {
println!("Ridimensionato a {}x{}", larghezza, altezza);
}
Evento::Chiudi => println!("Finestra chiusa"),
}
}
Pattern Annidati
I pattern possono essere annidati per gestire strutture dati complesse:
#[derive(Debug)]
enum Colore {
Rgb(u8, u8, u8),
Nome(String),
}
struct Forma {
tipo_forma: String,
colore: Option<Colore>,
}
fn descrivi_forma(forma: &Forma) {
match forma {
Forma {
tipo_forma,
colore: Some(Colore::Rgb(r, g, b)),
} => println!("{} con colore RGB({}, {}, {})", tipo_forma, r, g, b),
Forma {
tipo_forma,
colore: Some(Colore::Nome(nome)),
} => println!("{} di colore {}", tipo_forma, nome),
Forma {
tipo_forma,
colore: None,
} => println!("{} senza colore", tipo_forma),
}
}
fn main() {
let cerchio = Forma {
tipo_forma: String::from("Cerchio"),
colore: Some(Colore::Rgb(255, 0, 0)),
};
descrivi_forma(&cerchio);
}
Destrutturazione di Tuple
Le tuple si destrutturano naturalmente nel pattern matching:
fn classifica_coordinate(coord: (i32, i32)) {
match coord {
(0, 0) => println!("Origine"),
(x, 0) | (0, x) => println!("Su un asse, valore: {}", x),
(x, y) if x == y => println!("Sulla diagonale a ({}, {})", x, y),
(x, y) if x > 0 && y > 0 => println!("Primo quadrante"),
(x, y) if x < 0 && y > 0 => println!("Secondo quadrante"),
(x, y) => println!("Punto generico ({}, {})", x, y),
}
}
fn main() {
classifica_coordinate((0, 0));
classifica_coordinate((5, 0));
classifica_coordinate((3, 3));
classifica_coordinate((2, 7));
classifica_coordinate((-1, 4));
}
Binding con @ (At)
L’operatore @ permette di catturare un valore in una variabile mentre lo si testa contro un pattern:
fn classifica_eta(eta: u32) {
match eta {
0 => println!("Neonato"),
e @ 1..=5 => println!("Infanzia: {} anni", e),
e @ 6..=13 => println!("Bambino: {} anni", e),
e @ 14..=17 => println!("Adolescente: {} anni", e),
e @ 18..=64 => println!("Adulto: {} anni", e),
e @ 65.. => println!("Anziano: {} anni", e),
}
}
fn main() {
classifica_eta(3);
classifica_eta(15);
classifica_eta(42);
classifica_eta(70);
}
Il binding @ e utile anche con gli enum:
#[derive(Debug)]
enum Messaggio {
Ciao { id: i32 },
Addio,
}
fn main() {
let msg = Messaggio::Ciao { id: 5 };
match msg {
Messaggio::Ciao { id: id_val @ 3..=7 } => {
println!("ID nel range 3-7: {}", id_val);
}
Messaggio::Ciao { id } => println!("Altro ID: {}", id),
Messaggio::Addio => println!("Addio"),
}
}
ref e ref mut
I pattern ref e ref mut creano riferimenti invece di spostare i valori:
fn main() {
let nome = String::from("Rust");
// ref crea un riferimento nel pattern
match nome {
ref n => println!("Riferimento a: {}", n),
}
// nome e ancora utilizzabile
println!("Nome: {}", nome);
// ref mut per riferimenti mutabili
let mut valore = Some(42);
match valore {
Some(ref mut v) => {
*v += 10;
println!("Valore modificato: {}", v);
}
None => println!("Nessun valore"),
}
println!("Dopo modifica: {:?}", valore);
}
Pattern in let, if let, while let e for
I pattern non si usano solo in match. Sono pervasivi in tutto il linguaggio:
fn main() {
// Pattern in let
let (x, y, z) = (1, 2, 3);
let Punto { x: px, y: py } = Punto { x: 1.0, y: 2.0 };
println!("Punto: ({}, {})", px, py);
// if let per un singolo pattern
let valore: Option<i32> = Some(42);
if let Some(n) = valore {
println!("Trovato: {}", n);
}
// while let per iterare con pattern
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("Estratto: {}", top);
}
// Pattern nei for loop
let coppie = vec![(1, 'a'), (2, 'b'), (3, 'c')];
for (numero, lettera) in &coppie {
println!("{}: {}", numero, lettera);
}
// Pattern nei parametri di funzione
fn stampa_coordinata(&(x, y): &(i32, i32)) {
println!("Coordinata: ({}, {})", x, y);
}
stampa_coordinata(&(10, 20));
}
struct Punto {
x: f64,
y: f64,
}
Pattern Irrefutabili vs Refutabili
I pattern in Rust si dividono in due categorie:
- Irrefutabili: corrispondono sempre (es.
let x = 5,let (a, b) = (1, 2)) - Refutabili: possono non corrispondere (es.
Some(x),42)
fn main() {
// let richiede pattern irrefutabili
let x = 5; // OK: sempre valido
let (a, b) = (1, 2); // OK: sempre valido
// if let e match accettano pattern refutabili
let valore: Option<i32> = Some(10);
if let Some(n) = valore {
println!("Valore: {}", n);
}
// Questo NON compilerebbe:
// let Some(n) = valore; // ERRORE: pattern refutabile in contesto irrefutabile
// Ma si puo con let-else (Rust edition 2021+)
let Some(n) = valore else {
println!("Nessun valore");
return;
};
println!("Valore estratto con let-else: {}", n);
}
Guard nelle Match Arms
Le guard (if dopo il pattern) aggiungono condizioni extra:
fn classifica_numero(n: i32) {
match n {
n if n < 0 => println!("{} e negativo", n),
0 => println!("Zero"),
n if n % 2 == 0 => println!("{} e pari positivo", n),
n => println!("{} e dispari positivo", n),
}
}
fn main() {
classifica_numero(-5);
classifica_numero(0);
classifica_numero(4);
classifica_numero(7);
}
Conclusione
Il pattern matching in Rust e uno strumento estremamente versatile che permea l’intero linguaggio. Dalla destrutturazione di struct e enum ai binding con @, dalle guard condition ai pattern in let, for e parametri di funzione, padroneggiare il pattern matching e essenziale per scrivere codice Rust idiomatico. La distinzione tra pattern irrefutabili e refutabili aiuta a capire dove ciascun tipo di pattern puo essere usato, e il compilatore guida sempre verso un uso corretto.