|
L'argomento di questo paragrafo (forse avrei dovuto presentarlo prima, lo so) ci propone una modalità di analisi, destrutturazione e controllo di una grande varietà di dati. Si tratta di uno strumento così duttile che è difficile trovarne una definizione univoca che rende completamente l'idea del suo potenziale d'uso. A livello pratico permette un confronto tra strutture dati e pattern, ricordando un po' l'istruzione switch di altri linguaggi o il case del Pascal ma in formato molto più potente. Inoltre costituisce un valida alternativa ad if-else nei casi più complessi. Insomma osservarne il funzionamento pratico è la cosa migliore. Una definizione base che ci fa intuire linea generale il funzionamento è il seguente: match valore { Pattern1 => azione1, Pattern2 => azione2, ... _ => azione_default, } Una caratteristica importante del pattern matching è che il suo sviluppo deve essere esaustivo, ovvero non devono essere lasciati "casi irrisolti". Questo è il significato dell'ultimo branch dove quell'identificatore _ ha il senso di definire un'azione di default per tutti i casi non compresi in precedenza. Questo ricorda l'istruzione else finale nel costrutto if-else visto nel paragrafo precedente solo che in questo caso è obbligatorio comprendere tutti i casi possibili, laddove con if è possibile considerare solo alcuni casi. Iniziamo con un esempio semplice:
In questo semplice codice viene richiesto all'utente di inserire un numero. A seconda che tale input sia 1, 2 o un altro numero avremo a video la stampa del relativo messaggio. Come vedete, alla riga 14 abbiamo messo il rastrello che gestisce tutti i casi in cui l'input non sia 1 o 2. Se evidenzaste come commento la riga 14 , escludendola quindi dalla compilazione, il compilatore si farebbe sentire in maniera come sempre molto chiara: error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered --> r253.rs:11:7 | 11 | match x01 { | ^^^ patterns `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered | = note: the matched value is of type `i32` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms | 13 ~ 2 => println!("Numero 2"), 14 ~ i32::MIN..=0_i32 | 3_i32..=i32::MAX => todo!(), | La riga in grassetto vi dice chiaramente che non vi è una gestione esaustiva dei pattern di confronto esistente, dandovi anche ulteriori informazioni aggiuntive. Una precisazione ovvia ma importante è che solo un ramo può venire eseguito, non è possibile che ne vadano in esecuzione due o più. In altri linguaggi c'è modo, attraverso l'uso di label e istruzioni tipo goto, di saltare da un ramo all'altro di strutture come ad esempio gli switch in C#. In Rust, per quanto ne so, questo non è possibile con il pattern matching e se anche lo fosse non lo farei mai. Pertanto non è neanche possibile il fall-through (non servono i break) e quando viene eseguito un ramo il controllo esce immediatamente dal match. Un'altra osservazione è che è possibile lasciare un ramo senza alcuna azione da compiere, ad esempio 1 => {}, non fa nulla ed è perfettamente legale. Una prima domanda che potreste farvi è come gestire il caso in cui vi siano più istruzioni nei rami che andranno in esecuzione. Niente di nuovo, si usano le parentesi graffe, come al solito: pattern => { istruzioni }, con la virgola dopo la parentesi graffa di chiusura, ad esempio potreste modificare la riga 12 come segue: 1 => { println!("Numero 1"); println!("ovvero il primo numero"); }, Un altro caso comune è quello in cui ogni ramo debba prendere in esame più di un valore, ad esempio un range o insieme anche non contiguo. Vediamo qualche esempio, tralascio la parte di richiesta di input che appesantisce il codice, eventualmente potete sempre riprendere tale parte dal primo esempio:
Come si vede alle righe 4 e 5 sono stati inseriti dei range (vedi capitolo) di valori e cambiando il valore alla riga 2 otterrete output diversi. Le righe 4 e 5 presentano invece due varibiabili, quelle 'n' che trovate all'inizio che in congiunzione con if costituiscono le cosiddette "guardie" del pattern. La variabile n cattura il valore di x e lo utilizza per verificare la condizione di appartenenza al range tramite il metodo contains proprio dei range. Un altro frammento di codice che illustra l'uso di una guardia è il seguente: let x = 5; match x { n if n > 0 => println!("{} è positivo", n), n => println!("{} non è positivo", n), } Questo punto è piuttosto interessante e direi che merita un altro esempio:
Altro caso ancora è quello in cui il pattern è costituito da una serie di valori casuali:
usando l'operatore | (or) risolviamo il problema. Anche da un pattern matching Rust fa uscire il valore, siamo pur sempre di fronte ad una espressione in questo linguaggio, valore che possiamo attribuire ad esempio ad una variabile. Modifichiamo l'esempio precedente:
Alla riga 3 abbiamo provveduto ad assegnare alla variabile s01 il risultato del match. A questo punto, stante la grande importanza e utilità di questo argomento, riprendiamo procedendo in maniera strutturale esaminando come lavorare con alcuni costrutti di uso comune, considerando che la materia dovrebbe essere abbastanza chiara. Iniziamo con una semplice operazione su un enumerativo:
Vediamo ora un semplice esempio che coinvolge una ennupla:
E vediamone un ulteriore che coinvolge una struct:
E infine vediamome uno un po' più complesso che riassume un po' di cose.
Come si può vedere il matching aiuta a selezionare in maniera elegante le diverse scelte. In questo ultimo caso non serve il ramo "rastrello" in quanto tutte le eventualità previste dall'enumerativo sono contemplate nell'insieme dei vari branch. Nell'esempio vediamo ancora come lavorare con enumerativi e strutture. Tutti gli esempi presentati ritengo siano abbastanza semplici da capire, tranne l'ultimo la cui analisi lascio un po' tipo esercizio conclusione. Concludiamo, a proposito dell'identficatore _ che esso non ha solo il ruolo di introdurre il rastrello finale che fa su tutto quello che resta, esso è utile anche per ignorare alcuni valori che non sono di interesse. Riprendo l'esempio che si trova anche sul sito ufficiale in quanto semplice ed esplicativo:
Una variante di questa esclusione fa uso dell'operatore .. (due punti consecutivi, si vedono poco)
Come si vede il materiale è davvero ampio e ulteriori esempi li abbiamo visti e li vedremo ancora, ma questo perchè quando si parla di pattern matching si parla di un meccanismo davvero trasversale per il linguaggio. |