Toate locurile unde pattern-urile pot fi utilizate

Pattern-urile pot fi întâlnite în mai multe locuri în Rust, fiind utilizate frecvent fără a ne da seama! În această secțiune, vom vedea toate locurile unde aceste pattern-uri sunt valide.

Ramurile match

După cum am explicat în Capitolul 6, pattern-urile sunt utilizate în ramurile expresiilor match. Formal, expresiile match sunt definite prin cuvântul cheie match, o valoare cu care se face potrivirea, și unul sau mai multe ramuri de match care conțin un pattern și o expresie ce va fi executată dacă valoarea se potrivește cu pattern-ul respectivului ram, astfel:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

De exemplu, aceasta este expresia match din Listarea 6-5 care face potrivirea pe o valoare Option<i32> în variabila x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

Pattern-urile din această expresie match sunt None și Some(i), care se află în partea stângă a fiecărei săgeți.

O cerință pentru expresiile match este ca acestea să fie exhaustive, adică toate posibilitățile pentru valoarea subiect a expresiei match să fie acoperite. Una dintre metodele de a asigura că toate posibilitățile sunt cuprinse este de a include un pattern universal pentru ultimul ram: de exemplu, folosirea unui nume de variabilă care se potrivește cu orice valoare nu va da greș niciodată, cuprinzând astfel fiecare caz nemenționat anterior.

Pattern-ul specific _ va potrivi orice, însă nu se va lega de vreo variabilă, așa că deseori este utilizat în ultimul ram de match. Pattern-ul _ poate fi folositor atunci când dorim să ignorăm orice valoare care nu a fost specificată anterior, de exemplu. Vom analiza pattern-ul _ în mai multe detalii în secțiunea „Ignorarea valorilor într-un pattern”, mai târziu în acest capitol.

Expresii condiționale if let

În Capitolul 6, am discutat utilizarea expresiilor if let ca o formă mai concisă pentru a scrie echivalentul unei expresii match care se potrivește doar cu un singur caz. Opțional, if let poate include și un bloc else, care conține codul ce va fi executat dacă pattern-ul din if let nu se potrivește.

Listarea 18-1 demonstrează că este posibil să combinăm și să alternăm expresiile if let, else if, și else if let. Aceasta ne oferă o flexibilitate sporită în comparație cu o expresie match, unde putem compara o singură valoare cu pattern-urile. Mai mult, Rust nu solicită ca condițiile dintr-o serie de verificări if let, else if, else if let să fie interconectate.

Codul prezentat în Listarea 18-1 stabilește ce culoare să folosim pentru fond bazat pe o serie de condiții multiple. În acest exemplu, am definit variabile cu valori prestabilite, așa cum într-un program real acestea ar putea fi primite din intrările utilizatorilor.

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

Listarea 18-1: Alternanța expresiilor if let, else if, else if let, și else

Dacă utilizatorul indică o culoare preferată, aceasta este utilizată ca fond. Dacă nu este specificată nicio culoare preferată și astăzi e marți, culoarea de fond va fi verde. În caz contrar, dacă utilizatorul specifică vârsta sa ca string și putem să o convertim cu succes într-un număr, culoarea va fi ori mov, ori portocaliu, în funcție de valoarea numărului. Dacă niciuna dintre aceste condiții nu este îndeplinită, culoarea de fond va fi albastru.

Structura condițională descrisă ne permite gestionarea unor cerințe complexe. Având în vedere valorile prestabilite din acest exemplu, rezultatul va fi afișarea mesajului Using purple as the background color.

Reiese că expresiile if let pot introduce variabile umbrite în aceeași manieră ca brațele unui match: linia cu if let Ok(age) = age introduce o nouă variabilă umbrită age, care stochează valoarea din varianta Ok. De aceea, condiția if age > 30 trebuie să se afle în blocul respectiv de cod: nu putem combina aceste două condiții în if let Ok(age) = age && age > 30, căci variabila umbrită age pe care dorim să o comparăm cu 30 nu devine validă decât în momentul inițierii noului domeniu de vizibilitate cu acolada.

Un dezavantaj al folosirii if let este că compilatorul nu verifică exhaustivitatea, spre deosebire de expresiile match, care sunt verificate. Dacă am omite blocul else final, ratând astfel tratarea unor cazuri, compilatorul nu ne-ar semnala posibila greșeală de logică.

Bucle condiționale while let

Asemenea construcției if let, bucla condițională while let permite buclei while să fie executată cât timp un pattern continuă să fie potrivit. În Listarea 18-2, implementăm o buclă while let care folosește un vector ca stivă și afișează valorile din vector în ordinea inversă în care au fost introduse.

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

Listarea 18-2: Utilizarea unei bucle while let pentru a afișa valori atâta timp cât stack.pop() returnează Some

Acest exemplu afișează 3, 2 și apoi 1. Metoda pop înlătură ultimul element din vector și returnează Some(value). Dacă vectorul este gol, pop returnează None. Bucala while continuă execuția codului din blocul său cât timp pop returnează Some. Când pop returnează None, bucla se oprește. Utilizăm while let pentru a elimina pe rând fiecare element din stiva noastră.

Buclele for

Într-o buclă for, valoarea care urmează imediat după cuvântul cheie for este un pattern. De exemplu, în for x in y, x este pattern-ul. Listarea 18-3 ne arată cum folosim un pattern într-o buclă for pentru a destructura o tuplă ca parte a execuției buclei for.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{} is at index {}", value, index);
    }
}

Listarea 18-3: Utilizarea unui pattern într-o buclă for pentru a destrucura o tuplă

Codul din Listarea 18-3 va afișa următorul output:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

Modificăm un iterator cu metoda enumerate pentru ca acesta să ofere o valoare și indicele acelei valori într-o tuplă. Prima valoare generată este tupla (0, 'a'). Când această valoare potrivește cu pattern-ul (index, value), index va fi 0, iar value va fi 'a', ceea ce rezultă în afișarea primei linii a output-ului.

Declarațiile let

Până la acest capitol, am discutat explicit despre utilizarea pattern-urilor doar cu match și if let, dar de fapt, am aplicat pattern-uri și în alte contexte, inclusiv în declarațiile let. De exemplu, să luăm în considerare această atribuire simplă de variabilă cu let:

#![allow(unused)]
fn main() {
let x = 5;
}

De fiecare dată când ai utilizat o declarație let cum este aceasta, ai folosit pattern-uri, chiar dacă poate nu ai realizat acest lucru! Mai formal, o declarație let se prezintă în felul următor:

let PATTERN = EXPRESSION;

În declarații precum let x = 5; cu un nume de variabilă în poziția PATTERN, numele variabilei este de fapt o formă extrem de simplificată de pattern. Rust corespunde expresia cu pattern-ul și atribuie orice nume identifică. Așadar, în exemplul let x = 5;, x este un pattern care înseamnă “asociază ce se potrivește aici variabilei x.” Întrucât x reprezintă întregul pattern, acesta de fapt înseamnă “asociază orice valoare variabilei x, indiferent de aceasta.”

Pentru a înțelege mai clar aspectul de potrivire de pattern într-o declarație let, să ne referim la Listarea 18-4, unde se utilizează un pattern cu let pentru a destructura o tuplă.

fn main() {
    let (x, y, z) = (1, 2, 3);
}

Listarea 18-4: Utilizând un pattern pentru a destrucura o tuplă și a crea trei variabile simultan

În acest caz, se face un match între o tuplă și un pattern. Rust compară valoarea (1, 2, 3) cu pattern-ul (x, y, z) și observă că valoarea se potrivește cu pattern-ul, astfel Rust leagă 1 la x, 2 la y, și 3 la z. Acest pattern de tuplă poate fi văzut ca o compoziție a trei pattern-uri individuale de variabila.

Dacă numărul elementelor din pattern nu se potrivește cu numărul elementelor din tuplă, tipul total nu va corespunde și vom întâlni o eroare de compilare. De exemplu, Listarea 18-5 prezintă o tentativă de a destructura o tuplă cu trei elemente în doar două variabile, ceea ce nu va funcționa.

fn main() {
    let (x, y) = (1, 2, 3);
}

Listarea 18-5: Crearea incorectă a unui pattern în care variabilele nu corespund cu numărul de elemente din tuplă

Încercarea de a compila acest cod rezultă în următoarea eroare de tip:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` due to previous error

Pentru a rezolva eroarea, putem omite una sau mai multe valori ale tuplei folosind _ sau .., cum vom vedea în secțiunea „Ignorarea valorilor într-un pattern”. Dacă problema este că avem prea multe variabile în pattern, soluția constă în a ajusta tipurile prin eliminarea variabilelor până când numărul de variabile este egal cu numărul elementelor din tuplă.

Parametrii funcțiilor

Parametrii funcțiilor pot de asemenea să fie pattern-uri. Codul din Listarea 18-6, care declară o funcție denumită foo ce primește un parametru numit x de tip i32, ar trebui să ne fie acum cunoscut.

fn foo(x: i32) {
    // code goes here
}

fn main() {}

Listarea 18-6: O semnătură de funcție utilizează pattern-uri în parametri

Partea cu x reprezintă un pattern! Așa cum am procedat cu let, se poate potrivi o tuplă cu un pattern în argumentele unei funcții. Listarea 18-7 demonstrează despărțirea valorilor dintr-o tuplă pe când sunt transmise unei funcții.

Numele fișierului: src/main.rs

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Listarea 18-7: O funcție cu parametri ce destructurează o tuplă

Acest fragment de cod afișează Current location: (3, 5). Valorile &(3, 5) se potrivesc cu pattern-ul &(x, y), astfel x devine valoarea 3 iar y, valoarea 5.

Putem folosi de asemenea și pattern-uri în listele de parametri ale închiderilor în mod similar cu listele de parametri ale funcțiilor, având în vedere că închiderile sunt asemănătoare cu funcțiile, așa cum am discutat în Capitolul 13.

Ajuns aici, am avut ocazia să vedem câteva metode de utilizare a pattern-urilor, însă acestea nu funcționează identic în toate locurile în care le putem folosi. În unele situații, pattern-urile trebuie să fie irefutabile, în timp ce în altele pot fi refutabile. Vom explora aceste două concepte în detaliu în continuare.