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 con nome 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 copriranno quelle con lo stesso nome all’esterno
dei costrutti, come nel 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
. Osserviamo i
pattern nel campo match
e println!
alla fine, e cerchiamo di capire
cosa stamperà il codice prima di eseguirlo o di proseguire con la lettura.
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}"); }
match
con un braccio che introduce una nuova variabile che oscura una variabile esistente y
Esaminiamo cosa succede quando viene eseguita l’espressione match
. Il pattern
nel primo braccio 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 ambito all’interno
dell’espressione match
, questa è una nuova variabile y
, non la y
che abbiamo dichiarato
all’inizio con il valore 10
. Questo nuovo y
corrisponderà a qualsiasi valore
all’interno di Some
, che è ciò che abbiamo in x
. Pertanto, questo nuovo y
si lega al
valore interno di Some
in x
. Quel valore è 5
, quindi l’espressione per
quel braccio viene eseguita e stampa Matched, y = 5
.
Se x
fosse stato un valore None
invece di Some(5)
, i pattern nei primi
due bracci non avrebbero trovato corrispondenza, quindi il valore avrebbe trovato corrispondenza con il
trattino basso. Non abbiamo introdotto la variabile x
nel pattern del
braccio con trattino basso, quindi la x
nell’espressione è ancora la x
esterna che non è stata
ombreggiata. In questo caso ipotetico, match
stamperebbe Default case, x = None
.
Quando l’espressione match
è terminata, il suo ambito 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. Parleremo
delle condizioni di controllo della corrispondenza più avanti in “Aggiungere espressioni condizionali con le condizioni di controllo della corrispondenza”.
Corrispondenza di più Pattern
Nelle espressioni match
, è possibile confrontare più pattern utilizzando la sintassi |
,
che è l’operatore di controllo della corrispondenza or. 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 uno dei valori in quel valore,
verrà eseguito il codice di quel valore:
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 un intervallo di valori inclusivo. Nel
codice seguente, quando un pattern corrisponde a uno qualsiasi dei valori all’interno dell’intervallo
indicato, quel braccio verrà eseguito:
fn main() { let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } }
Se x
è 1
, 2
, 3
, 4
o 5
, il primo braccio 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ù breve, 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 tipi 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!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } }
Rust can tell that 'c'
is within the first pattern’s range and prints early ASCII letter
.
Destructuring to Break Apart Values
We can also use patterns to destructure structs, enums, and tuples to use different parts of these values. Let’s walk through each value.
Structs
Listing 19-12 shows a Point
struct with two fields, x
and y
, that we can
break apart using a pattern with a let
statement.
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b); }
Questo codice crea le variabili a
e b
che corrispondono ai valori dei campi x
e y
della struttura p
. Questo esempio mostra che i nomi delle
variabili nel pattern non devono necessariamente corrispondere ai nomi dei campi della struttura.
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 Point { x: x, y: y } = p;
contiene
molte duplicazioni, Rust ha una scorciatoia per i pattern che corrispondono ai campi della struttura:
è sufficiente elencare il nome del campo della struttura 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
.
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y); }
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 Point
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.
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("Sull'asse x a {x}"), Point { x: 0, y } => println!("Sull'asse y a {y}"), Point { x, y } => { println!("Su nessun asse: ({x}, {y})"); } } }
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 Point
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 Point { 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. 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.
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!("Il 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}"); } } }
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 bracci.
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 braccio. Qui usiamo la forma abbreviata come
abbiamo fatto nel Listato 19-13.
Per varianti di enum di tipo tuple, 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.
Strutture ed Enumerazioni Annidate
Finora, i nostri esempi hanno tutti confrontato strutture o enumerazioni con un solo livello di profondità,
ma la corrispondenza può funzionare anche su elementi annidati! Ad esempio, possiamo rifattorizzare 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}"); } _ => (), } }
Il pattern del primo braccio nell’espressione match
corrisponde a una
variante dell’enumerazione Messaggio::CambiaColore
che contiene una variante Colore::Rgb
; quindi
il pattern si lega ai tre valori i32
interni. Il pattern del secondo
braccio corrisponde anche 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.
Strutture 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 strutture e tuple all’interno di una tupla e destrutturamo 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 tipi complessi nelle loro parti 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 struttura, separatamente l’uno dall’altro.
Ignorare Valori In un Pattern
Avete visto che a volte è utile ignorare i valori in un pattern, come
nell’ultimo ramo di un match
, per ottenere un catch-all 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 avete 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 Valore Intero con _
Abbiamo utilizzato il trattino basso come pattern jolly 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.
fn foo(_: i32, y: i32) { println!("Questa funzione utilizza solo il parametro y: {y}"); } fn main() { foo(3, 4); }
_
in una firma di funzioneQuesto 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 tratto per il quale è necessaria una certa firma di tipo, 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 annullata.
fn main() { let mut valore_setting = Some(5); let nuovo_valore_setting = Some(10); match (valore_setting, nuovo_valore_setting) { (Some(_), Some(_)) => { println!("Non è possibile sovrascrivere un valore personalizzato esistente"); } _ => { valore_setting = nuovo_valore_setting; } } println!("Valore setting è {valore_setting:?}"); }
Some
quando non è necessario utilizzare il valore all’interno di Some
Questo codice stamperà Non è possibile sovrascrivere un valore personalizzato esistente
e poi
Valore setting è 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 valore_setting
e nuovo_valore_setting
siano la variante Some
. In tal
caso, stampiamo il motivo per cui valore_setting
non viene modificato, e non viene
modificato.
In tutti gli altri casi (se valore_setting
o nuovo_valore_setting
è None
)
espressi dal pattern _
nel secondo braccio, vogliamo consentire a nuovo_valore_setting
di diventare valore_setting
.
Possiamo anche utilizzare caratteri di sottolineatura 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}"); } } }
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 sta appena iniziando un progetto. In questa situazione, puoi dire a Rust di non avvisarti della variabile inutilizzata iniziando 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.
fn main() { let _x = 5; let y = 10; }
Qui, riceviamo un avviso sul mancato utilizzo della variabile y
, ma non riceviamo un avviso sul mancato utilizzo di _x
.
Si noti 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:?}");
}
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:?}"); }
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 caratteri di sottolineatura 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
struttura Punto
che contiene una coordinata 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}"), } }
Point
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
strutture 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.
fn main() { let numeri = (2, 4, 8, 16, 32); match numeri { (primo, .., ultimo) => { println!("Alcuni numeri: {primo}, {ultimo}"); } } }
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 ..
, quindi non verrà
compilato.
fn main() {
let numeri = (2, 4, 8, 16, 32);
match numeri {
(.., secondo, ..) => {
println!("Alcuni numeri: {secondo}")
},
}
}
..
in modo ambiguoQuando compiliamo questo esempio, otteniamo questo errore:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
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 da un solo pattern. Si noti,
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 => (), } }
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 di match guard.
Nel Listato 19-11, abbiamo accennato alla possibilità di utilizzare le match guards per risolvere il nostro
problema di pattern-shadowing. Ricordiamo che abbiamo 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.
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}"); }
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 della match
.
La clausola di controllo 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 clausola di controllo per specificare più
pattern; la condizione di controllo si applicherà a tutti i pattern. Il Listato
19-28 mostra la precedenza quando si combina un pattern che utilizza |
con una clausola di controllo. 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!("si"), _ => println!("no"), } }
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
|
, armil ramo avrebbe trovato una corrispondenza e il programma avrebbe stampato
yes
.
Utilizzo dei 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 un campo Message::Hello
id
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 { Hello { id: i32 }, } let msg = Messaggio::Hello { id: 5 }; match msg { Messaggio::Hello { id: id @ 3..=7 } => { println!("Trovato un id nell'intervallo: {id}") } Messaggio::Hello { id: 10..=12 } => { println!("Trovato un id in un altro intervallo") } Messaggio::Hello { id } => println!("Trovato un altro id: {id}"), } }
@
per associare un valore in un pattern e testarloQuesto 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.
Riepilogo
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 istruzioni 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.