00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.