Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sintassi dei Pattern

In questa sezione, raccogliamo tutta la sintassi valida nei pattern e discutiamo perché e quando potresti voler utilizzare ciascuna di esse.

Corrispondenza di Letterali

Come hai visto nel Capitolo 6, puoi confrontare i pattern direttamente con i letterali. Il codice seguente fornisce alcuni esempi:

fn main() {
    let x = 1;

    match x {
        1 => println!("uno"),
        2 => println!("due"),
        3 => println!("tre"),
        _ => println!("altro"),
    }
}

Questo codice stampa uno perché il valore in x è 1. Questa sintassi è utile quando vuoi che il codice esegua un’azione se ottiene un particolare valore concreto.

Corrispondenza di Variabili Denominate

Le variabili denominate sono pattern inconfutabili che corrispondono a qualsiasi valore e le abbiamo utilizzate molte volte in questo libro. Tuttavia, si verifica una complicazione quando si utilizzano variabili denominate nelle espressioni match, if let o while let. Poiché ciascuna di queste espressioni inizia un nuovo scope, le variabili dichiarate come parte di un pattern all’interno di queste espressioni oscureranno quelle con lo stesso nome all’esterno dei costrutti, come è il caso di tutte le variabili. Nel Listato 19-11, dichiariamo una variabile denominata x con il valore Some(5) e una variabile y con il valore 10. Creiamo quindi un’espressione match sul valore x. Osserva i pattern nel campo match e println! alla fine, e cerca di capire cosa stamperà il codice prima di eseguirlo o di proseguire con la lettura.

File: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Ricevuto 50"),
        Some(y) => println!("Corrisponde, y = {y}"),
        _ => println!("Caso predefinito, x = {x:?}"),
    }

    println!("alla fine: x = {x:?}, y = {y}");
}
Listato 19-11: Un’espressione match con un ramo che introduce una nuova variabile che oscura una variabile esistente y

Esaminiamo cosa succede quando viene eseguita l’espressione match. Il pattern nel primo ramo di corrispondenza non corrisponde al valore definito di x, quindi il codice continua.

Il pattern nel secondo ramo di corrispondenza introduce una nuova variabile denominata y che corrisponderà a qualsiasi valore all’interno di un valore Some. Poiché ci troviamo in un nuovo scope all’interno dell’espressione match, questa è una nuova variabile y, non la y che abbiamo dichiarato all’inizio con il valore 10. Questa nuova y corrisponderà a qualsiasi valore all’interno di Some, che è ciò che abbiamo in x. Pertanto, questa nuova y si lega al valore interno di Some in x. Quel valore è 5, quindi l’espressione per quel ramo viene eseguita e stampa Corrisponde, y = 5.

Se x fosse stato un valore None invece di Some(5), i pattern nei primi due rami non avrebbero trovato corrispondenza, quindi il valore avrebbe trovato corrispondenza con il trattino basso. Non abbiamo introdotto la variabile x nel pattern del ramo con trattino basso, quindi la x nell’espressione è ancora la x esterna che non è stata oscurata. In questo caso ipotetico, match stamperebbe Caso predefinito, x = None.

Quando l’espressione match termina, il suo scope termina, così come quello della y interna. L’ultimo println! produce alla fine: x = Some(5), y = 10.

Per creare un’espressione match che confronti i valori delle variabili esterne x e y, anziché introdurre una nuova variabile che oscura la variabile y esistente, dovremmo usare una condizione di controllo della corrispondenza. Ne parleremo più avanti nella sezione “Aggiungere Istruzioni Condizionali con le Match Guard”.

Corrispondenza di Più Pattern

Nelle espressioni match, è possibile confrontare più pattern utilizzando la sintassi |, che è l’operatore OR di controllo della corrispondenza. Ad esempio, nel codice seguente confrontiamo il valore di x con i valori corrispondenti, il primo dei quali ha un’opzione or, il che significa che se il valore di x corrisponde a l’uno o l’altro dei valori del ramo, verrà eseguito il codice di quel ramo:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("uno o due"),
        3 => println!("tre"),
        _ => println!("altro"),
    }
}

Questo codice stampa uno o due.

Corrispondenza di Intervalli di Valori con ..=

La sintassi ..= ci consente di confrontare con un intervallo di valori inclusivo. Nel codice seguente, quando un pattern corrisponde a uno qualsiasi dei valori all’interno dell’intervallo indicato, quel ramo verrà eseguito:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("da uno a cinque"),
        _ => println!("altro"),
    }
}

Se x è 1, 2, 3, 4 o 5, il primo ramo corrisponderà. Questa sintassi è più comoda per più valori di corrispondenza rispetto all’utilizzo dell’operatore | per esprimere la stessa idea; se dovessimo usare |, dovremmo specificare 1 | 2 | 3 | 4 | 5. Specificare un intervallo è molto più conciso, soprattutto se vogliamo trovare una corrispondenza, ad esempio, con un numero qualsiasi compreso tra 1 e 1.000!

Il compilatore verifica che l’intervallo non sia vuoto in fase di compilazione e, poiché gli unici type per cui Rust può stabilire se un intervallo è vuoto o meno sono char e i valori numerici, gli intervalli sono consentiti solo con valori numerici o char.

Ecco un esempio che utilizza intervalli di valori char:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("lettere ASCII iniziali"),
        'k'..='z' => println!("lettere ASCII finali"),
        _ => println!("altro"),
    }
}

Rust può capire che 'c' si trova all‘interno del primo intervallo e stamperà lettere ASCII iniziali.

Destrutturare per Separare Valori

Possiamo inoltre usare i pattern per destrutturare struct, enum e tuple per estrarre parti diverse di questi valori. Passiamo in rassegna ognuna di queste strutture dati.

Struct

Il Listato 19-12 mostra una struct Punto con due campi, x e y, che possiamo separare usando un pattern con una dichiarazione let.

File: src/main.rs
struct Punto {
    x: i32,
    y: i32,
}

fn main() {
    let p = Punto { x: 0, y: 7 };

    let Punto { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}
Listato 19-12: Destrutturazione dei campi di una struct in variabili separate

Questo codice crea le variabili a e b che corrispondono ai valori dei campi x e y della struct p. Questo esempio mostra che i nomi delle variabili nel pattern non devono necessariamente corrispondere ai nomi dei campi della struct. Tuttavia, è comune far corrispondere i nomi delle variabili ai nomi dei campi per rendere più facile ricordare quali variabili provengono da quali campi. A causa di questo uso comune, e poiché scrivere let Punto { x: x, y: y } = p; contiene molte duplicazioni, Rust ha una scorciatoia per i pattern che corrispondono ai campi della struct: è sufficiente elencare il nome del campo della struct e le variabili create dal pattern avranno gli stessi nomi. Il Listato 19-13 si comporta allo stesso modo del codice del Listato 19-12, ma le variabili create nel pattern let sono x e y invece di a e b.

File: src/main.rs
struct Punto {
    x: i32,
    y: i32,
}

fn main() {
    let p = Punto { x: 0, y: 7 };

    let Punto { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}
Listato 19-13: Destrutturazione dei campi struct tramite dichiarazione abbreviata

Questo codice crea le variabili x e y che corrispondono ai campi x e y della variabile p. Il risultato è che le variabili x e y contengono i valori della struct p.

Possiamo anche destrutturare con valori letterali come parte del pattern struct piuttosto che creare variabili per tutti i campi. In questo modo possiamo testare alcuni campi per valori specifici mentre creiamo variabili per destrutturare gli altri campi.

Nel Listato 19-14, abbiamo un’espressione match che separa i valori Punto in tre casi: punti che giacciono direttamente sull’asse x (che è vero quando y = 0), sull’asse y (x = 0) o su nessuno dei due assi.

File: src/main.rs
struct Punto {
    x: i32,
    y: i32,
}

fn main() {
    let p = Punto { x: 0, y: 7 };

    match p {
        Punto { x, y: 0 } => println!("Sull'asse x a {x}"),
        Punto { x: 0, y } => println!("Sull'asse y a {y}"),
        Punto { x, y } => println!("Su nessun asse: ({x}, {y})"),
    }
}
Listato 19-14: Destrutturazione e corrispondenza di valori letterali in un unico pattern

Il primo caso corrisponderà a qualsiasi punto che si trovi sull’asse x specificando che il campo y corrisponde se il suo valore corrisponde al letterale 0. Il pattern crea comunque una variabile x che possiamo utilizzare nel codice per questo ramo.

Analogamente, il secondo caso corrisponde a qualsiasi punto sull’asse y specificando che il campo x corrisponde se il suo valore è 0 e crea una variabile y per il valore del campo y. Il terzo ramo non specifica alcun letterale, quindi corrisponde a qualsiasi altro Punto e crea variabili per entrambi i campi x e y.

In questo esempio, il valore p corrisponde al secondo ramo in virtù del fatto che x contiene uno 0, quindi questo codice stamperà Sull’asse y a 7.

Ricorda che un’espressione match interrompe il controllo dei rami una volta trovato il primo pattern corrispondente, quindi anche se Punto { x: 0, y: 0 } si trova sull’asse x e sull’asse y, questo codice stamperà solo Sull'asse x a 0.

Enum

Abbiamo destrutturato gli enum in questo libro (ad esempio, Listato 6-5 nel Capitolo 6), ma non abbiamo ancora spiegato esplicitamente che il pattern per destrutturare un’enum corrisponde al modo in cui sono definiti i dati memorizzati all’interno dell’enum stesso. Ad esempio, nel Listato 19-15 utilizziamo l’enum Messaggio del Listato 6-2 e scriviamo un match con pattern che destruttureranno ogni valore interno.

File: src/main.rs
enum Messaggio {
    Esci,
    Muovi { x: i32, y: i32 },
    Scrivi(String),
    CambiaColore(i32, i32, i32),
}
// ANCHOR: here                                         
fn main() {
    let msg = Messaggio::CambiaColore(0, 160, 255);

    match msg {
        Messaggio::Esci => {
            println!("La variante Esci non ha dati da destrutturare.");
        }
        Messaggio::Muovi { x, y } => {
            println!("Muovi in direzione x {x} e in direzione y {y}");
        }
        Messaggio::Scrivi(text) => {
            println!("Messaggio di testo: {text}");
        }
        Messaggio::CambiaColore(r, g, b) => {
            println!("Cambia colore in rosso {r}, verde {g}, e blu {b}");
        }
    }
}
Listato 19-15: Destrutturazione delle varianti di enum che contengono diversi tipi di valori

Questo codice stamperà Cambia colore in rosso 0, verde 160 e blu 255. Prova a modificare il valore di msg per vedere l’esecuzione del codice degli altri rami.

Per le varianti di enum senza dati, come Messaggio::Esci, non possiamo destrutturare ulteriormente il valore. Possiamo trovare corrispondenze solo sul valore letterale Messaggio::Esci, e non ci sono variabili in quel pattern.

Per varianti di enum di tipo struct, come Messaggio::Muovi, possiamo usare un pattern simile a quello che specifichiamo per la corrispondenza con le struct. Dopo il nome della variante, inseriamo parentesi graffe e poi elenchiamo i campi con le variabili, in modo da separare i pezzi da usare nel codice per questo ramo. Qui usiamo la forma abbreviata come abbiamo fatto nel Listato 19-13.

Per varianti di enum di type tupla, come Messaggio::Scrivi che contiene una tupla con un elemento e Messaggio::CambiaColore che contiene una tupla con tre elementi, il pattern è simile a quello che specifichiamo per la corrispondenza con le tuple. Il numero di variabili nel pattern deve corrispondere al numero di elementi nella variante che stiamo correlando.

Struct ed Enum Annidate

Finora, i nostri esempi hanno tutti confrontato struct o enum con un solo livello di profondità, ma la corrispondenza può funzionare anche su elementi annidati! Ad esempio, possiamo riscrivere il codice nel Listato 19-15 per supportare i colori RGB e HSV nel messaggio CambiaColore come mostrato nel Listato 19-16.

enum Colore {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}
    
enum Messaggio {
    Esci,
    Muovi { x: i32, y: i32 },
    Scrivi(String),
    CambiaColore(Colore),
}

fn main() {
    let msg = Messaggio::CambiaColore(Colore::Hsv(0, 160, 255));

    match msg {
        Messaggio::CambiaColore(Colore::Rgb(r, g, b)) => {
            println!("Cambia colore in rosso {r}, verde {g}, e blu {b}");
        }
        Messaggio::CambiaColore(Colore::Hsv(h, s, v)) => {
            println!("Cambia colore in tonalità {h}, saturazione {s}, valore {v}");
        }
        _ => (),
    }
}
Listato 19-16: Corrispondenza su enum annidate

Il pattern del primo ramo nell’espressione match corrisponde a una variante dell’enum Messaggio::CambiaColore che contiene una variante Colore::Rgb; quindi il pattern si lega ai tre valori i32 interni. Anche il pattern del secondo ramo corrisponde a una variante dell’enum Messaggio::CambiaColore, ma l’enum interno corrisponde invece a Colore::Hsv. Possiamo specificare queste condizioni complesse in un’unica espressione match, anche se sono coinvolte due enum.

Struct e Tuple

Possiamo combinare, abbinare e annidare pattern di destrutturazione in modi ancora più complessi. L’esempio seguente mostra una destrutturazione complessa in cui annidiamo struct e tuple all’interno di una tupla e destrutturiamo tutti i valori primitivi:

fn main() {
    struct Punto {
        x: i32,
        y: i32,
    }

    let ((piedi, pollici), Punto { x, y }) = ((3, 10), Punto { x: 3, y: -10 });
}

Questo codice ci permette di scomporre i type complessi nelle loro componenti in modo da poter utilizzare i valori che ci interessano separatamente.

La destrutturazione con i pattern è un modo comodo per utilizzare parti di valori, come il valore di ciascun campo in una struct, separatamente l’uno dall’altro.

Ignorare Valori In un Pattern

Hai visto che a volte è utile ignorare i valori in un pattern, come nell’ultimo ramo di un match, per ottenere un pigliatutto che in realtà non fa nulla ma tiene conto di tutti i valori possibili rimanenti. Esistono diversi modi per ignorare interi valori o parti di valori in un pattern: utilizzare il pattern _ (che hai visto), utilizzare il pattern _ all’interno di un altro pattern, utilizzare un nome che inizia con un trattino basso o utilizzare .. per ignorare le parti rimanenti di un valore. Esploriamo come e perché utilizzare ciascuno di questi pattern.

Un Intero Valore con _

Abbiamo utilizzato il trattino basso come pattern pigliatutto che corrisponde a qualsiasi valore ma non si lega al valore. Questo è particolarmente utile come ultimo ramo in un’espressione match , ma possiamo utilizzarlo anche in qualsiasi pattern, inclusi i parametri di funzione, come mostrato nel Listato 19-17.

File: src/main.rs
fn foo(_: i32, y: i32) {
    println!("Questa funzione utilizza solo il parametro y: {y}");
}

fn main() {
    foo(3, 4);
}
Listato 19-17: Usare _ in una firma di funzione

Questo codice ignorerà completamente il valore 3 passato come primo argomento e stamperà Questa funzione usa solo il parametro y: 4.

Nella maggior parte dei casi, quando non è più necessario un particolare parametro di funzione, si modifica la firma in modo che non includa il parametro non utilizzato. Ignorare un parametro di funzione può essere particolarmente utile nei casi in cui, ad esempio, si sta implementando un trait per il quale è necessaria una certa firma di type, ma il corpo della funzione nell’implementazione non richiede uno dei parametri. Si evita così di ricevere un avviso del compilatore sui parametri di funzione non utilizzati, come si farebbe utilizzando un nome.

Parti di un Valore Con un _ Annidato

Possiamo anche usare _ all’interno di un altro pattern per ignorare solo una parte di un valore, ad esempio, quando vogliamo testare solo una parte di un valore ma non abbiamo bisogno delle altre parti nel codice corrispondente che vogliamo eseguire. Il Listato 19-18 mostra il codice responsabile della gestione del valore di un’impostazione. I requisiti aziendali prevedono che l’utente non possa sovrascrivere una personalizzazione esistente di un’impostazione, ma possa annullarla e assegnarle un valore se è attualmente non impostata.

fn main() {
    let mut val_impostazioni = Some(5);
    let nuovo_val_impostazioni = Some(10);

    match (val_impostazioni, nuovo_val_impostazioni) {
        (Some(_), Some(_)) => {
            println!("Non è possibile sovrascrivere un valore personalizzato esistente");
        }
        _ => {
            val_impostazioni = nuovo_val_impostazioni;
        }
    }

    println!("Valore impostazioni è {val_impostazioni:?}");
}
Listato 19-18: Utilizzo di un trattino basso all’interno di pattern che corrispondono alle varianti Some quando non è necessario utilizzare il valore all’interno di Some

Questo codice stamperà Non è possibile sovrascrivere un valore personalizzato esistente e poi Valore impostazioni è Some(5). Nel primo ramo di corrispondenza, non è necessario abbinare o utilizzare i valori all’interno di una delle varianti Some, ma è necessario testare il caso in cui val_impostazioni e nuovo_val_impostazioni siano la variante Some. In tal caso, stampiamo il motivo della mancata modifica di val_impostazioni, e quindi non viene modificato.

In tutti gli altri casi (se val_impostazioni o nuovo_val_impostazioni è None) espressi dal pattern _ nel secondo ramo, vogliamo consentire a nuovo_val_impostazioni di diventare val_impostazioni.

Possiamo anche utilizzare il trattino basso in più punti all’interno di un pattern per ignorare valori specifici. Il Listato 19-19 mostra un esempio di come ignorare il secondo e il quarto valore in una tupla di cinque elementi.

fn main() {
    let numeri = (2, 4, 8, 16, 32);

    match numeri {
        (primo, _, terzo, _, quinto) => {
            println!("Alcuni numeri: {primo}, {terzo}, {quinto}");
        }
    }
}
Listato 19-19: Ignorare più parti di una tupla

Questo codice stamperà Alcuni numeri: 2, 8, 32, e i valori 4 e 16 saranno ignorati.

Una Variabile Inutilizzata Iniziando il Suo Nome con _

Se si crea una variabile ma non la si utilizza da nessuna parte, Rust di solito genera un avviso perché una variabile inutilizzata potrebbe essere un bug. Tuttavia, a volte è utile poter creare una variabile che non si utilizzerà ancora, ad esempio quando si sta realizzando un prototipo o si è nelle prime fasi di un progetto. In questa situazione, puoi dire a Rust di non avvisarti della variabile inutilizzata facendo iniziare il nome della variabile con un trattino basso. Nel Listato 19-20, creiamo due variabili inutilizzate, ma quando compiliamo questo codice, dovremmo ricevere un avviso solo per una di esse.

File: src/main.rs
fn main() {
    let _x = 5;
    let y = 10;
}
Listato 19-20: Far iniziare il nome di una variabile con un trattino basso per evitare di ricevere avvisi di variabili inutilizzate

Qui, riceviamo un avviso sul mancato utilizzo della variabile y, ma non riceviamo un avviso sul mancato utilizzo di _x.

Nota che c’è una sottile differenza tra l’utilizzo di solo _ e l’utilizzo di un nome che inizia con un trattino basso. La sintassi _x vincola comunque il valore alla variabile, mentre _ non lo vincola affatto. Per mostrare un caso in cui questa distinzione è importante, il Listato 19-21 ci fornirà un errore.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("trovata una stringa");
    }

    println!("{s:?}");
}
Listato 19-21: Una variabile inutilizzata che inizia con un trattino basso vincola comunque il valore, che potrebbe assumerne la proprietà

Riceveremo un errore perché il valore s verrà comunque spostato in _s, il che ci impedisce di utilizzare nuovamente s. Tuttavia, l’utilizzo del trattino basso da solo non vincola mai il valore. Il Listato 19-22 verrà compilato senza errori perché s non viene spostato in _.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("trovata una stringa");
    }

    println!("{s:?}");
}
Listato 19-22: L’utilizzo di un trattino basso non vincola il valore

Questo codice funziona perfettamente perché non vincola mai s a nulla; non viene spostato.

Parti Rimanenti di un Valore con ..

Con valori composti da molte parti, possiamo usare la sintassi .. per usare parti specifiche e ignorare il resto, evitando la necessità di elencare trattini bassi per ogni valore ignorato. Il pattern .. ignora qualsiasi parte di un valore che non abbiamo corrisposto esplicitamente nel resto del pattern. Nel Listato 19-23, abbiamo una struct Punto che contiene coordinate nello spazio tridimensionale. Nell’espressione match, vogliamo operare solo sulla coordinata x e ignorare i valori nei campi y e z.

fn main() {
    struct Punto {
        x: i32,
        y: i32,
        z: i32,
    }

    let origine = Punto { x: 0, y: 0, z: 0 };

    match origine {
        Punto { x, .. } => println!("x è {x}"),
    }
}
Listato 19-23: Ignorare tutti i campi di un Punto tranne x usando ..

Elenchiamo il valore x e poi includiamo semplicemente il pattern ... Questo è più veloce che dover elencare y: _ e z: _, soprattutto quando lavoriamo con struct che hanno molti campi in situazioni in cui solo uno o due campi sono rilevanti.

La sintassi .. si espanderà a tutti i valori necessari. Il Listato 19-24 mostra come usare .. con una tupla.

File: src/main.rs
fn main() {
    let numeri = (2, 4, 8, 16, 32);

    match numeri {
        (primo, .., ultimo) => {
            println!("Alcuni numeri: {primo}, {ultimo}");
        }
    }
}
Listato 19-24: Corrispondenza solo del primo e dell’ultimo valore in una tupla e ignorando tutti gli altri valori

In questo codice, il primo e l’ultimo valore vengono confrontati con primo e ultimo. .. corrisponderà e ignorerà tutto ciò che si trova nel mezzo.

Tuttavia, l’utilizzo di .. deve essere univoco. Se non è chiaro quali valori siano destinati alla corrispondenza e quali debbano essere ignorati, Rust restituirà un errore. Il Listato 19-25 mostra un esempio di utilizzo ambiguo di .., che quindi non verrà compilato.

File: src/main.rs
fn main() {
    let numeri = (2, 4, 8, 16, 32);

    match numeri {
        (.., secondo, ..) => {
            println!("Alcuni numeri: {secondo}")
        },
    }
}
Listato 19-25: Un tentativo di utilizzare .. in modo ambiguo

Quando compiliamo questo esempio, otteniamo questo errore:

$ cargo run
   Compiling patterns v0.1.0 (file:///progetti/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:23
  |
5 |         (.., secondo, ..) => {
  |          --           ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error

È impossibile per Rust determinare quanti valori nella tupla ignorare prima di abbinare un valore con secondo e poi quanti altri valori ignorare successivamente. Questo codice potrebbe significare che vogliamo ignorare 2, associare secondo a 4 e quindi ignorare 8, 16 e 32; oppure che vogliamo ignorare 2 e 4, associare secondo a 8 e quindi ignorare 16 e 32; e così via. Il nome della variabile secondo non ha alcun significato particolare in Rust, quindi otteniamo un errore del compilatore perché usare .. in due punti come questo è ambiguo.

Aggiungere Istruzioni Condizionali con le Match Guard

Una match guard è una condizione if aggiuntiva, specificata dopo il pattern in un ramo match, che deve corrispondere affinché quel ramo venga scelto. Le match guard sono utili per esprimere idee più complesse di quelle consentite dal solo pattern. Nota, tuttavia, che sono disponibili solo nelle espressioni match, non nelle espressioni if let o while let.

La condizione può utilizzare variabili create nel pattern. Il Listato 19-26 mostra un match in cui il primo ramo ha il pattern Some(x) e ha anche una match guard if x % 2 == 0 (che sarà true se il numero è pari).

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("Il numero {x} è pari"),
        Some(x) => println!("Il numero {x} è dispari"),
        None => (),
    }
}
Listato 19-26: Aggiungere una match guard a un pattern

Questo esempio stamperà Il numero 4 è pari. Quando num viene confrontato con il pattern nel primo ramo, corrisponde perché Some(4) corrisponde a Some(x). Quindi la match guard controlla se il resto della divisione di x per 2 è uguale a 0 e, poiché lo è, viene selezionato il primo ramo.

Se num fosse stato Some(5), la match guard nel primo ramo sarebbe stata false perché il resto di 5 diviso 2 è 1, che è diverso da 0. Rust passerebbe quindi al secondo ramo, che corrisponderebbe perché il secondo ramo non ha una match guard e quindi corrisponde a qualsiasi variante di Some.

Non c’è modo di esprimere la condizione if x % 2 == 0 all’interno di un pattern, quindi la match guard ci dà la possibilità di esprimere questa logica. Lo svantaggio di questa espressività aggiuntiva è che il compilatore non cerca di verificare l’esaustività quando sono coinvolte espressioni con match guard.

Nel Listato 19-11, avevamo accennato alla possibilità di utilizzare le match guard per risolvere il nostro problema di oscuramento della variabili. Ricorda che avevamo creato una nuova variabile all’interno del pattern nell’espressione match invece di utilizzare la variabile esterna a match. Questa nuova variabile ci impediva di testare il valore della variabile esterna. Il Listato 19-27 mostra come possiamo usare una match guard per risolvere questo problema.

File: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Ricevuto 50"),
        Some(n) if n == y => println!("Corrisponde, n = {n}"),
        _ => println!("Caso predefinito, x = {x:?}"),
    }

    println!("alla fine: x = {x:?}, y = {y}");
}
Listato 19-27: Usare una match guard per verificare l’uguaglianza con una variabile esterna

Questo codice ora stamperà Caso predefinito, x = Some(5). Il pattern nel secondo ramo di corrispondenza non introduce una nuova variabile y che oscurerebbe la y esterna, il che significa che possiamo usare la y esterna nella match guard. Invece di specificare il pattern come Some(y), che avrebbe oscurato la y esterna, specifichiamo Some(n). Questo crea una nuova variabile n che non oscura nulla perché non esiste alcuna variabile n al di fuori del match.

La match guard if n == y non è un pattern e quindi non introduce nuove variabili. Questa y è la y esterna anziché una nuova y che la oscura, e possiamo cercare un valore che abbia lo stesso valore della y esterna confrontando n con y.

È anche possibile utilizzare l’operatore or | in una match guard per specificare più pattern; la match guard si applicherà a tutti i pattern. Il Listato 19-28 mostra la precedenza quando si combina un pattern che utilizza | con una match guard. La parte importante di questo esempio è che la match guard if y si applica a 4, 5 e 6, anche se potrebbe sembrare che if y si applichi solo a 6.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("sì"),
        _ => println!("no"),
    }
}
Listato 19-28: Combinazione di più pattern con una match guard

La condizione di corrispondenza stabilisce che il ramo corrisponde solo se il valore di x è uguale a 4, 5 o 6 e se y è true. Quando questo codice viene eseguito, il pattern del primo ramo corrisponde perché x è 4, ma la match guard if y è false, quindi il primo ramo non viene scelto. Il codice passa al secondo ramo, che corrisponde, e questo programma stampa no. Il motivo è che la condizione if si applica all’intero pattern 4 | 5 | 6, non solo all’ultimo valore 6. In altre parole, la precedenza di una match guard rispetto a un pattern si comporta in questo modo:

(4 | 5 | 6) if y => ...

piuttosto che in questo modo:

4 | 5 | (6 if y) => ...

Dopo aver eseguito il codice, il comportamento della precedenza è evidente: se la match guard fosse stata applicata solo al valore finale nell’elenco di valori specificato utilizzando l’operatore |, il ramo avrebbe trovato una corrispondenza e il programma avrebbe stampato .

Utilizzare il Binding @

L’operatore at @ ci consente di creare una variabile che contiene un valore mentre stiamo testando quel valore per una corrispondenza con il pattern. Nel Listato 19-29, vogliamo verificare che il campo id di Messaggio::Ciao sia compreso nell’intervallo 3..=7. Vogliamo anche associare il valore alla variabile id in modo da poterlo utilizzare nel codice associato al ramo.

fn main() {
    enum Messaggio {
        Ciao { id: i32 },
    }

    let msg = Messaggio::Ciao { id: 5 };

    match msg {
        Messaggio::Ciao { id: id @ 3..=7 } => {
            println!("Trovato un id nell'intervallo: {id}")
        }
        Messaggio::Ciao { id: 10..=12 } => {
            println!("Trovato un id in un altro intervallo")
        }
        Messaggio::Ciao { id } => println!("Trovato un altro id: {id}"),
    }
}
Listato 19-29: Usare @ per associare un valore in un pattern e testarlo

Questo esempio stamperà Trovato un id nell'intervallo: 5. Specificando id @ prima dell’intervallo 3..=7, catturiamo qualsiasi valore corrispondente all’intervallo in una variabile denominata id, verificando anche che il valore corrisponda al pattern dell’intervallo.

Nel secondo ramo, dove abbiamo specificato solo un intervallo nel pattern, il codice associato al ramo non ha una variabile che contenga il valore effettivo del campo id. Il valore del campo id avrebbe potuto essere 10, 11 o 12, ma il codice associato a quel pattern non sa quale sia. Il codice del pattern non è in grado di utilizzare il valore del campo id, perché non abbiamo salvato il valore id in una variabile.

Nell’ultimo ramo, dove abbiamo specificato una variabile senza intervallo, abbiamo il valore disponibile da utilizzare nel codice del ramo in una variabile denominata id. Il motivo è che abbiamo utilizzato la sintassi abbreviata del campo struct. Ma non abbiamo applicato alcun test al valore del campo id in questo ramo, come abbiamo fatto con i primi due rami: qualsiasi valore corrisponderebbe a questo pattern.

L’utilizzo di @ ci consente di testare un valore e salvarlo in una variabile all’interno di un pattern.

I pattern di Rust sono molto utili per distinguere tra diversi tipi di dati. Quando vengono utilizzati nelle espressioni match, Rust garantisce che i pattern coprano ogni valore possibile, altrimenti il programma non verrà compilato. I pattern nelle dichiarazioni let e nei parametri di funzione rendono questi costrutti più utili, consentendo la destrutturazione dei valori in parti più piccole e l’assegnazione di tali parti a variabili. Possiamo creare pattern semplici o complessi in base alle nostre esigenze.

Successivamente, nel penultimo capitolo del libro, esamineremo alcuni aspetti avanzati di una varietà di funzionalità di Rust.