A pattern matching nem véletlenül az egyik első dolog, amit minden Rust-tanuló megismer, aztán évekig újra és újra rátalál benne valami újat. Ez a cikk végigmegy a match, az if let és a while let konstrukciókon, és megmutatja, hogyan lehet velük tiszta, biztonságos és kimerítő (exhaustive) logikát írni.

A match kifejezés alapjai és kimerítő ellenőrzés

A match kifejezés lényege, hogy a fordító garantálja, hogy minden lehetséges esetet lefedtél. Ez nem csak stílus kérdése – ez egy fordítási időben ellenőrzött biztonsági garancia.

fn leiras(szam: i32) -> &'static str {
    match szam {
        0 => "nulla",
        n if n < 0 => "negatív",
        1..=9 => "egyjegyű pozitív",
        _ => "nagy szám",
    }
}

fn main() {
    for x in [-5, 0, 3, 42] {
        println!("{x}: {}", leiras(x));
    }
}

Ha kihagynád az _ ágat, a fordító hibát dob, mert az i32 értéktartománya nem lenne lefedve. Ez a kimerítőségi ellenőrzés (exhaustiveness checking) az, amitől a match sokkal biztonságosabb, mint egy hagyományos switch C-ben vagy Java-ban.

Tipp

Ha egy enum-hoz később új variánst adsz hozzá, és van rá match a kódodban _ ág nélkül, a fordító azonnal jelezni fogja, hogy hol kell kiegészítened a logikát. Ez az egyik legjobb barátod refaktoráláskor.

Mintaőrök (guard) és összetett minták

A minták önmagukban gyakran nem elég kifejezőek – ilyenkor jönnek jól a guard-ok, azaz a mintához kapcsolt extra feltételek az if kulcsszóval.

enum Esemeny {
    Kattintas { x: i32, y: i32 },
    Gorgetes(i32),
    Billentyu(char),
}

fn feldolgoz(e: Esemeny) {
    match e {
        Esemeny::Kattintas { x, y } if x < 0 || y < 0 => {
            println!("Érvénytelen koordináta: ({x}, {y})");
        }
        Esemeny::Kattintas { x, y } => {
            println!("Kattintás itt: ({x}, {y})");
        }
        Esemeny::Gorgetes(delta) if delta > 0 => println!("Görgetés felfelé: {delta}"),
        Esemeny::Gorgetes(delta) => println!("Görgetés lefelé: {delta}"),
        Esemeny::Billentyu(c @ 'a'..='z') => println!("Kisbetű: {c}"),
        Esemeny::Billentyu(c) => println!("Egyéb karakter: {c}"),
    }
}

Itt két érdekes dolgot is látsz: a guard-ot (if x < 0 || y < 0), és a @ bindinget, amivel egyszerre tudsz mintát illeszteni és az illesztett értéket egy változóba menteni. Az 1..=9 vagy 'a'..='z' tartománymegadás is teljesen természetes része a mintáknak.

Az | operátorral több mintát is összefűzhetsz egyetlen ágban:

fn hetvege(nap: u8) -> bool {
    matches!(nap, 6 | 7)
}

A matches! makró egy nagyon praktikus rövidítés, ha csak egy bool eredményre van szükséged egy mintaillesztésből.

Megjegyzés

A guard-oknál figyelj arra, hogy a fordító nem tudja garantálni a kimerítőséget olyan biztonsággal, mint guard nélkül – ha egy guard sosem igaz valamelyik ágra, azt neked kell észrevenned, a fordító ebben nem segít.

Destrukturálás struktúrákon és enumokon

A pattern matching igazi ereje a destrukturálásban rejlik: egy összetett típusból egyszerre több mezőt is kibonthatsz, akár egymásba ágyazva is.

struct Pont {
    x: i32,
    y: i32,
}

enum Alakzat {
    Kor { kozeppont: Pont, sugar: f64 },
    Teglalap { bal_felso: Pont, jobb_also: Pont },
}

fn terulet(alakzat: &Alakzat) -> f64 {
    match alakzat {
        Alakzat::Kor { sugar, .. } => std::f64::consts::PI * sugar * sugar,
        Alakzat::Teglalap {
            bal_felso: Pont { x: x1, y: y1 },
            jobb_also: Pont { x: x2, y: y2 },
        } => ((x2 - x1) * (y2 - y1)).abs() as f64,
    }
}

A .. szintaxis lehetővé teszi, hogy csak azokra a mezőkre koncentrálj, amik éppen érdekelnek – a kör esetében a középpont koordinátáira most nincs szükségünk, csak a sugárra.

Struktúrák destrukturálásánál gyakran hasznos a mezőnév-átnevezés is (x: x1), különösen ha egymásba ágyazott típusokból bontasz ki több hasonló nevű mezőt.

let Pont { x, y } = Pont { x: 3, y: 7 };
println!("x={x}, y={y}");

Ez utóbbi egy egyszerű let destrukturálás – itt nincs is szükség match-re, mert a Pont struktúra minden mintája garantáltan illeszkedik (irrefutable pattern).

if let és while let a rövidebb kódért

Ha csak egyetlen mintára vagy kíváncsi, a teljes match túlzás lenne. Ilyenkor az if let a barátod:

fn feldolgoz_opcio(ertek: Option<i32>) {
    if let Some(n) = ertek {
        println!("Van érték: {n}");
    } else {
        println!("Nincs érték");
    }
}

Még tovább egyszerűsíthetsz a let-else szintaxissal, amely az 1.65-ös verzió óta stabil, és kiválóan alkalmas az "early return" mintára:

fn feldolgoz_kotelezo(ertek: Option<i32>) -> i32 {
    let Some(n) = ertek else {
        println!("Hiányzó érték, alapérték használata");
        return 0;
    };
    n * 2
}

A while let pedig akkor jön jól, ha egy mintaillesztést kell ismételten végrehajtanod, amíg az illeszkedik – tipikusan iterátoroknál vagy verem-szerű struktúráknál.

fn main() {
    let mut verem = vec![1, 2, 3, 4, 5];

    while let Some(top) = verem.pop() {
        println!("Levettem: {top}");
    }
}
Jó tudni

Az if let és a while let nem kimerítő minta-ellenőrzést végez – csak az adott mintára reagál, minden más esetben csendben átlépnek felette (kivéve, ha van else ág). Ha kimerítő logikára van szükséged, a match a helyes választás.

Gyakorlati példa: állapotgép implementálása pattern matchinggel

Nézzünk egy összefoglaló, valós(abb) példát: implementáljunk egy egyszerű állapotgépet egy kapcsoló (traffic light) szimulálására, ahol a match felelős az állapotátmenetek logikájáért.

#[derive(Debug, Clone, Copy, PartialEq)]
enum Allapot {
    Piros,
    Sarga,
    Zold,
}

#[derive(Debug)]
enum Esemeny {
    Idozito,
    Hiba(String),
}

fn atmenet(jelenlegi: Allapot, esemeny: Esemeny) -> Allapot {
    match (jelenlegi, esemeny) {
        (Allapot::Piros, Esemeny::Idozito) => Allapot::Zold,
        (Allapot::Zold, Esemeny::Idozito) => Allapot::Sarga,
        (Allapot::Sarga, Esemeny::Idozito) => Allapot::Piros,
        (_, Esemeny::Hiba(uzenet)) => {
            println!("Hiba történt: {uzenet}, visszaállás Piros állapotra");
            Allapot::Piros
        }
    }
}

fn main() {
    let mut allapot = Allapot::Piros;

    let esemenyek = vec![
        Esemeny::Idozito,
        Esemeny::Idozito,
        Esemeny::Hiba("szenzorhiba".to_string()),
        Esemeny::Idozito,
    ];

    for esemeny in esemenyek {
        println!("Előtte: {allapot:?}, esemény: {esemeny:?}");
        allapot = atmenet(allapot, esemeny);
        println!("Utána: {allapot:?}\n");
    }
}

Ez a mintázat – egy tuple-be csomagolt (jelenlegi_allapot, esemeny) páron végzett match – rendkívül elegáns módja az állapotgépek leírásának. Minden állapotátmenet egyetlen helyen, deklaratívan van megfogalmazva, és ha új állapotot vagy eseményt adsz hozzá, a fordító azonnal jelzi, ha valamilyen kombinációt elfelejtettél kezelni (feltéve, hogy nincs _ a mintában, amely mindent elnyel).

Figyelem

Az Esemeny::Hiba(_) ágban a (_, Esemeny::Hiba(uzenet)) minta minden állapotra illeszkedik – ez praktikus, de ha bővíted az enumot új eseménytípussal, a _ az állapot helyén elfedheti azt, hogy valamelyik konkrét kombinációt kezelni kellene. Legyél óvatos az általános _-mintákkal, ha a kimerítőségi ellenőrzés előnyeit maximálisan ki akarod használni.

Ha a projekt komplexitása nő, érdemes lehet a Allapot és Esemeny típusokat egy dedikált modulba szervezni, és az atmenet függvényt egy impl blokkba tenni – ez semmit nem változtat a pattern matching logikáján, csak a kód strukturáltságát javítja.

Összefoglalás

A pattern matching a Rust egyik legkifejezőbb eszköze: a match kimerítő ellenőrzést és biztonságos elágazást ad, a guard-ok és összetett minták finomhangolják a logikát, a destrukturálás pedig lehetővé teszi, hogy elegánsan bontsd szét a struktúráidat és enumjaidat. Az if let, while let és a let-else a mindennapi, kis léptékű mintaillesztéseket teszik tömörebbé, anélkül hogy feláldoznád a típusbiztonságot. Ha legközelebb egy if-else if-else láncot vagy egy bonyolult feltételrendszert kellene írnod, gondolkodj el azon, hogy a match és társai nem adnának-e tisztább, biztonságosabb megoldást.