Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories
 Corso Gratuito di Programmazione Rust Lezione 021 – Il costrutto del flusso di controllo della partita

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Il costrutto di controllo match

In Rust c’è un potente costrutto di controllo chiamato match che ti consente di confrontare un valore con una serie di modelli e poi eseguire del codice in base a quale modello corrisponde. I modelli possono essere composti da valori letterali, nomi di variabili, caratteri jolly e molte altre cose; il Capitolo 18 copre tutti i diversi tipi di modelli e cosa fanno. La potenza di match deriva dall’espressività dei modelli e dal fatto che il compilatore conferma che tutti i casi possibili siano gestiti.

Pensa a un’espressione di match come a una macchina per la selezione delle monete: le monete scivolano lungo un binario con buchi di varie dimensioni lungo di esso, e ogni moneta cade nel primo buco che trova in cui si adatta. Allo stesso modo, i valori passano attraverso ogni modello in un match, e al primo modello in cui il valore “si adatta”, il valore cade nel blocco di codice associato per essere utilizzato durante l’esecuzione.

Parlando di monete, utilizziamole come esempio usando il match! Possiamo scrivere una funzione che prende una moneta US sconosciuta e, in modo simile alla macchina per il conteggio, determina quale moneta è e restituisce il suo valore in centesimi, come mostrato nel Listato 6-3.

rust

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

Listato 6-3: Un enum e un’espressione di match che ha le varianti dell’enum come i suoi modelli

Analizziamo il match nella funzione value_in_cents. Prima elenchiamo la parola chiave match seguita da un’espressione, che in questo caso è il valore coin. Questo sembra molto simile a un’espressione condizionale usata con if, ma c’è una grande differenza: con if, la condizione deve valutarsi a un valore booleano, ma qui può essere qualsiasi tipo. Il tipo di moneta in questo esempio è l’enum Coin che abbiamo definito nella prima riga.

Successivamente ci sono i rami di match. Un ramo ha due parti: un modello e del codice. Il primo ramo qui ha un modello che è il valore Coin::Penny e poi l’operatore => che separa il modello e il codice da eseguire. Il codice in questo caso è solo il valore 1. Ogni ramo è separato dal successivo da una virgola.

Quando l’espressione di match viene eseguita, confronta il valore risultante con il modello di ciascun ramo, in ordine. Se un modello corrisponde al valore, viene eseguito il codice associato a quel modello. Se quel modello non corrisponde al valore, l’esecuzione continua al ramo successivo, proprio come in una macchina per la selezione delle monete. Possiamo avere quanti rami vogliamo: nel Listato 6-3, il nostro match ha quattro rami.

Il codice associato a ciascun ramo è un’espressione, e il valore risultante dell’espressione nel ramo corrispondente è il valore che viene restituito per l’intera espressione di match.

Solitamente non usiamo le parentesi graffe se il codice del ramo di match è breve, come nel Listato 6-3 dove ogni ramo restituisce solo un valore. Se vuoi eseguire più righe di codice in un ramo di match, devi usare le parentesi graffe, e la virgola che segue il ramo diventa quindi facoltativa. Ad esempio, il seguente codice stampa “Penny fortunata!” ogni volta che il metodo viene chiamato con un Coin::Penny, ma restituisce comunque l’ultimo valore del blocco, 1:

rust

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Penny fortunata!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

Modelli che vincolano i valori

Un’altra caratteristica utile dei rami di match è che possono vincolare le parti dei valori che corrispondono al modello. È così che possiamo estrarre i valori dalle varianti di enum.

Come esempio, cambiamo una delle nostre varianti di enum per contenere dati al suo interno. Dal 1999 al 2008, gli Stati Uniti coniarono quarti con disegni diversi per ciascuno dei 50 stati su un lato. Nessun’altra moneta ebbe disegni di stato, quindi solo i quarti hanno questo valore extra. Possiamo aggiungere queste informazioni al nostro enum cambiando la variante Quarter per includere un valore UsState memorizzato al suo interno, come abbiamo fatto nel Listato 6-4.

rust

#[derive(Debug)] // così possiamo ispezionare lo stato tra un minuto
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

Listato 6-4: Un enum Coin in cui la variante Quarter tiene anche un valore UsState

Immaginiamo che un amico stia cercando di collezionare tutti i quarti di stato. Mentre ordiniamo il nostro resto di monete per tipo, chiameremo anche il nome dello stato associato a ciascun quarto in modo che se è uno dei quarti che il nostro amico non ha, possa aggiungerlo alla sua collezione.

Nell’espressione di match per questo codice, aggiungiamo una variabile chiamata state al modello che corrisponde ai valori della variante Coin::Quarter. Quando un Coin::Quarter corrisponde, la variabile state verrà vincolata al valore dello stato di quel quarto. Poi possiamo usare state nel codice per quel ramo, così:

rust

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("Quarto di stato da {:?}!", state);
25
}
}
}

Se dovessimo chiamare value_in_cents(Coin::Quarter(UsState::Alaska)), coin sarebbe Coin::Quarter(UsState::Alaska). Quando confrontiamo quel valore con ciascun ramo di match, nessuno di essi corrisponde finché non raggiungiamo Coin::Quarter(state). A quel punto, il vincolo per lo state sarà il valore UsState::Alaska. Possiamo quindi usare quel vincolo nell’espressione println!, ottenendo così il valore interno dello stato dalla variante di enum Coin per il Quarter.

Confronto con Option<T>

Nella sezione precedente, volevamo ottenere il valore interno T dal caso Some quando si utilizza Option<T>; possiamo anche gestire Option<T> utilizzando match, come abbiamo fatto con l’enum Coin! Invece di confrontare le monete, confrontiamo le varianti di Option<T>, ma il funzionamento dell’espressione di match rimane lo stesso.

Supponiamo di voler scrivere una funzione che prenda un Option<i32> e, se c’è un valore interno, aggiunga 1 a quel valore. Se non c’è un valore interno, la funzione dovrebbe restituire il valore None e non tentare di eseguire alcuna operazione.

Questa funzione è molto semplice da scrivere, grazie a match, e avrà questo aspetto:

rust

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Listato 6-5: Una funzione che utilizza un’espressione di match su un Option<i32>

Esaminiamo più nel dettaglio la prima esecuzione di plus_one. Quando chiamiamo plus_one(five), la variabile x nel corpo di plus_one avrà il valore Some(5). Confrontiamo quindi quello con ciascun ramo di match:

css

None => None,

Il valore Some(5) non corrisponde al modello None, quindi continuiamo con il prossimo ramo:

scss

Some(i) => Some(i + 1),

Some(5) corrisponde a Some(i)? Si! Abbiamo la stessa variante. L’i si lega al valore contenuto in Some, quindi i assume il valore 5. Il codice nel ramo di match viene quindi eseguito, quindi aggiungiamo 1 al valore di i e creiamo un nuovo valore Some con il nostro totale 6 al suo interno.

Ora consideriamo la seconda chiamata di plus_one nel Listato 6-5, dove x è None. Entriamo nel match e confrontiamo il primo ramo:

css

None => None,

Corrisponde! Non c’è alcun valore a cui aggiungere, quindi il programma si interrompe e restituisce il valore None sul lato destro di =>. Poiché il primo ramo ha corrisposto, non vengono confrontati altri rami.

Combinare match ed enum è utile in molte situazioni. Vedrai questo modello spesso nel codice Rust: confrontare un enum, vincolare una variabile ai dati interni e quindi eseguire del codice in base ad esso. È un po’ complicato all’inizio, ma una volta che ci si abitua, lo si vorrebbe in tutti i linguaggi. È costantemente uno dei preferiti dagli utenti.

I match sono esaustivi

C’è un altro aspetto di match che dobbiamo discutere: i modelli dei rami devono coprire tutte le possibilità. Considera questa versione della nostra funzione plus_one, che ha un bug e non verrà compilata:

[Questo codice non compila!]

rust

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

Non abbiamo gestito il caso None, quindi questo codice causerà un bug. Fortunatamente, è un bug che Rust sa come catturare. Se proviamo a compilare questo codice, otterremo questo errore:

$ cargo run Compilazione enums v0.1.0 (file:///projects/enums) errore[E0004]: modelli non esaustivi: None non coperti –> src/main.rs:3:15 | 3 | match x { | ^ modello None non coperto | nota: Option<i32> definito qui –> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1 | = nota: /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: non coperto = nota: il valore corrispondente è di tipo Option<i32> aiuto: assicurati che tutti i casi possibili vengano gestiti aggiungendo un braccio di match con un modello jolly o un modello esplicito come mostrato | 4 ~ Some(i) => Some(i + 1), 5 ~ None => todo!(), |

Per maggiori informazioni su questo errore, prova rustc --explain E0004. errore: non è possibile compilare enums a causa di un errore precedente

Rust sa che non abbiamo coperto ogni possibile caso, e sa anche quale modello abbiamo dimenticato! I match in Rust sono esaustivi: dobbiamo esaurire ogni possibilità affinché il codice sia valido. Specialmente nel caso di Option<T>, quando Rust ci impedisce di dimenticare di gestire esplicitamente il caso None, ci protegge dall’assumere di avere un valore quando potremmo avere null, rendendo così impossibile il miliardo di dollari di errore discusso in precedenza.

Modelli generici e il segnaposto _

Utilizzando gli enum, possiamo anche eseguire azioni speciali per alcuni valori particolari, ma per tutti gli altri valori eseguire una determinata azione predefinita. Immagina di implementare un gioco in cui, se ottieni un 3 su un lancio di dado, il tuo giocatore non si muove, ma invece ottiene un nuovo cappello alla moda. Se ottieni un 7, il tuo giocatore perde un cappello alla moda. Per tutti gli altri valori, il tuo giocatore si muove di un certo numero di spazi sulla plancia di gioco. Ecco un match che implementa quella logica, con il risultato del lancio di dado codificato invece di un valore casuale, e tutta la logica rappresentata da funzioni senza corpo perché effettivamente implementarle è fuori tema per questo esempio:

rust

let lancio_dado = 9;
match lancio_dado {
3 => aggiungi_cappello_moda(),
7 => rimuovi_cappello_moda(),
altro => muovi_giocatore(altro),
}

fn aggiungi_cappello_moda() {}
fn rimuovi_cappello_moda() {}
fn muovi_giocatore(num_spazi: u8) {}

Per i primi due rami, i modelli sono i valori letterali 3 e 7. Per l’ultimo ramo che copre ogni altro valore possibile, il modello è la variabile che abbiamo scelto di chiamare altro. Il codice che viene eseguito per l’altro ramo utilizza la variabile passandola alla funzione move_player.

Questo codice viene compilato, anche se non abbiamo elencato tutti i possibili valori che un u8 può avere, perché l’ultimo modello corrisponderà a tutti i valori non specificamente elencati. Questo modello di cattura di tutti incontra il requisito che il match debba essere esaustivo. Nota che dobbiamo mettere l’ultimo ramo di cattura alla fine perché i modelli vengono valutati nell’ordine. Se mettiamo il ramo di cattura iniziale, gli altri rami non verranno mai eseguiti, quindi Rust ci avvertirà se aggiungiamo rami dopo un cattura-tutto!

Rust ha anche un modello che possiamo utilizzare quando vogliamo una cattura-tutto ma non vogliamo utilizzare il valore nel modello di cattura-tutto: _ è un modello speciale che corrisponde a qualsiasi valore e non si collega a quel valore. Questo dice a Rust che non useremo il valore, quindi Rust non ci avvertirà di una variabile non utilizzata.

Cambiamo le regole del gioco: ora, se ottieni qualsiasi altra cosa oltre a un 3 o un 7, devi tirare di nuovo. Non dobbiamo più usare il valore di cattura-tutto, quindi possiamo cambiare il nostro codice per utilizzare _ invece della variabile denominata altro:

rust

let lancio_dado = 9;
match lancio_dado {
3 => aggiungi_cappello_moda(),
7 => rimuovi_cappello_moda(),
_ => tira_di_nuovo(),
}

fn aggiungi_cappello_moda() {}
fn rimuovi_cappello_moda() {}
fn tira_di_nuovo() {}

Questo esempio soddisfa anche il requisito di esaustività perché ignoriamo esplicitamente tutti gli altri valori nell’ultimo ramo; non abbiamo dimenticato nulla.

Infine, cambieremo ancora le regole del gioco in modo che non accada nulla al tuo turno se ottieni qualsiasi cosa diversa da un 3 o un 7. Possiamo esprimere ciò utilizzando il valore unitario (il tipo di tupla vuota che abbiamo menzionato nella sezione “Il tipo di tupla”) come il codice che accompagna il ramo _:

rust

let lancio_dado = 9;
match lancio_dado {
3 => aggiungi_cappello_moda(),
7 => rimuovi_cappello_moda(),
_ => (),
}

fn aggiungi_cappello_moda() {}
fn rimuovi_cappello_moda() {}

Qui, stiamo dicendo esplicitamente a Rust che non useremo nessun altro valore che non corrisponda a un modello in un ramo precedente, e non vogliamo eseguire alcun codice in questo caso.

C’è altro sui modelli e il matching che tratteremo nel Capitolo 18. Per ora, passeremo alla sintassi if let, che può essere utile in situazioni in cui l’espressione di match è un po’ verbosa.

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *