A pattern matching nem csak egy szintaktikai csecsebecse a Rustban, hanem a nyelv gondolkodásának a szíve. Amikor match-et írsz, a fordító garantálja, hogy minden lehetséges esetet lekezeltél – ez a kimerítő mintaillesztés (exhaustiveness checking), és ez az, amitől a Rust kód sokkal biztonságosabb, mint egy hagyományos switch-case bármelyik nyelven.

A match kifejezés alapjai

A match kifejezés egy értéket vesz, és sorban végignézi a megadott mintákat, amíg talál egyet, ami illik rá. A legfontosabb szabály: minden lehetséges esetet le kell fedni, különben a kód nem fordul.

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));
    }
}

Figyeld meg, hogy az 1..=9 egy tartományminta (range pattern), az n if n < 0 pedig egy guard feltétel – erről később bővebben. A _ a "minden más" jolly joker, ami sokszor kötelező, ha a mintáid nem fedik le a típus összes lehetséges értékét.

Tipp

Ha egy enum-on matchelsz, és később új variánst adsz hozzá, a fordító azonnal figyelmeztet minden helyen, ahol nem kezelted le az új esetet – feltéve, hogy nincs _ ág, ami "elnyeli" a hibát. Ezért kezdetben inkább kerüld a _-t enumoknál, ha teheted.

Destrukturálás struct-okon és enum-okon

A match igazi ereje a destrukturálásban rejlik. Nem csak azt tudod megnézni, milyen variánsa van egy enumnak, hanem egyszerre ki is tudod bontani a benne lévő adatokat.

enum Uzenet {
    Kilepes,
    Mozgas { x: i32, y: i32 },
    Szoveg(String),
    Szin(u8, u8, u8),
}

fn feldolgoz(uz: Uzenet) {
    match uz {
        Uzenet::Kilepes => println!("Kilépés kérve"),
        Uzenet::Mozgas { x, y } => println!("Mozgás: ({x}, {y})"),
        Uzenet::Szoveg(s) if s.is_empty() => println!("Üres szöveg érkezett"),
        Uzenet::Szoveg(s) => println!("Szöveg: {s}"),
        Uzenet::Szin(r, g, b) => println!("Szín: #{r:02x}{g:02x}{b:02x}"),
    }
}

Struct-okon ugyanez a logika érvényes: a mezőket egyenként kibonthatod, vagy ..-vel jelezheted, hogy a maradék mezőket nem érdekel figyelembe venni.

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

fn eredetben_van(p: &Pont) -> bool {
    matches!(p, Pont { x: 0, y: 0, .. })
}

fn main() {
    let p = Pont { x: 0, y: 0, z: 42 };
    println!("{}", eredetben_van(&p));
}

A matches! makró egy nagyon hasznos rövidítés, ha csak egy bool-t akarsz visszakapni arról, hogy egy érték illik-e egy mintára – nem kell hozzá teljes match blokkot írni.

if let és while let – amikor nem kell minden ág

Gyakori helyzet, hogy csak egy mintára vagy érdekelve, a többi esetben pedig nem akarsz semmit csinálni. Erre tökéletes az if let.

fn main() {
    let valasz: Option<i32> = Some(7);

    if let Some(ertek) = valasz {
        println!("Kaptunk egy értéket: {ertek}");
    } else {
        println!("Nincs érték");
    }
}

Ha iteratívan szeretnél mintákat illeszteni – például amíg egy Vec-ből tudsz elemet kivenni –, a while let a barátod:

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

    while let Some(top) = verem.pop() {
        println!("Kivett elem: {top}");
    }
}

Ez sokkal olvashatóbb, mint egy loop + match + break kombináció.

Megjegyzés

Ha a mintaillesztés sikertelensége esetén korai visszatérést (early return) szeretnél, érdemes megnézni a let-else szintaxist, ami az 1.65-ös verzió óta stabil:

fn feldolgoz_sort(sor: &str) -> Option<i32> {
    let Ok(szam) = sor.trim().parse::<i32>() else {
        println!("Nem sikerült feldolgozni: {sor}");
        return None;
    };
    Some(szam * 2)
}

A let-else pontosan azt az esetet fedi le, amikor az if let else ágában mindig el kell térned a jelenlegi kontroll-folyamból (return, break, continue, panic) – így elkerülöd a felesleges beágyazást.

Guard feltételek és @ kötések

A guard feltétel (match ... if ...) egy extra bool-kifejezés, amit a minta illesztése után ellenőriz a fordító. Fontos tudni, hogy a guard nem vesz részt a kimerítőség ellenőrzésében – tehát ha egy guard sosem igaz, a fordító attól még úgy gondolja, hogy az az ág lefedi az esetet.

fn kategorizal(x: i32) -> &'static str {
    match x {
        n if n % 2 == 0 && n % 3 == 0 => "osztható 6-tal",
        n if n % 2 == 0 => "páros",
        n if n % 3 == 0 => "hármas",
        _ => "egyéb",
    }
}

Az @ kötés (binding) akkor jön jól, amikor egy tartományra vagy mintára szeretnél illeszteni, de közben az egész értéket is meg akarod tartani egy névben.

fn eletkor_kategoria(kor: u8) -> String {
    match kor {
        gyerek @ 0..=12 => format!("gyerek, {gyerek} éves"),
        tini @ 13..=17 => format!("tinédzser, {tini} éves"),
        felnott @ 18..=64 => format!("felnőtt, {felnott} éves"),
        idos => format!("idős, {idos} éves"),
    }
}

Ez különösen hasznos, ha a mintában lévő struktúrát is destrukturálni akarod, miközben az egész értéket is szeretnéd elérni:

#[derive(Debug)]
struct Csomag {
    id: u32,
    suly: f32,
}

fn ellenoriz(csomag: Csomag) {
    match csomag {
        c @ Csomag { suly, .. } if suly > 20.0 => {
            println!("Túl nehéz csomag: {c:?}");
        }
        c => println!("Rendben van: {c:?}"),
    }
}
Jó tudni

Az @ kötést nem lehet több almintával kombinálni úgy, hogy azok különböző neveket kössenek be – vagyis x @ (A(a) | B(b)) esetén a és b nem elérhető az x-en kívül, csak akkor, ha mindkét alminta ugyanazt a nevet adja. Ez gyakori zavart okoz kezdőknél.

Gyakori hibák kezdőknél és hogyan kerüljük el őket

1. Elfelejtett kimerítőség miatt túl sok _ ág. Sokan reflexből tesznek _ => {} ágat minden match végére, még akkor is, ha a típus mindössze két-három variánst tartalmaz. Ez elrejti azokat a hibákat, amikor egy új enum variánst adsz hozzá, és elfelejtesz lekezelni. Csak akkor használj _-t, ha valóban szükséges.

2. Move vs. borrow keveredés matchelésnél. Ha egy nem-Copy típusú értéket matchelsz match ertek { ... } formában, az érték move-olódik a mintákba. Ha utána még szükséged van rá, matchelj referenciára: match &ertek { ... }, vagy használj ref kötést.

fn main() {
    let nev = String::from("Rúzsa");

    match &nev {
        s if s.starts_with('R') => println!("R-re kezdődik: {s}"),
        _ => println!("Más betűvel kezdődik"),
    }

    // nev még itt is használható, mert csak referenciát matcheltünk
    println!("Teljes név: {nev}");
}

3. if let láncolása mélyen egymásba ágyazva. Ha több Option-t vagy Result-ot kell egymás után kibontanod, könnyen "pyramid of doom" alakulhat ki. Ilyenkor érdemes a ? operátort, vagy a let-else-t alkalmazni a korai visszatéréshez, mielőtt a beágyazás kikezelhetetlenné válna.

4. A guard feltétel túlterhelése. Ha egy guard feltétel túl bonyolult logikát tartalmaz, jobb kiszervezni egy külön függvénybe – a match ág olvashatósága sokkal fontosabb, mint hogy minden egy sorban legyen.

Figyelem

Ne feledd, hogy a match ágak sorrendje számít! A Rust felülről lefelé próbálja illeszteni a mintákat, és az első illő ágat választja. Ha egy szélesebb mintát (pl. _ vagy egy guard nélküli tartomány) fentebb helyezel el egy specifikusabbnál, a specifikusabb ág soha nem fog lefutni – és a fordító ezt sajnos nem minden esetben jelzi hibaként (bár gyakran ad unreachable pattern figyelmeztetést).

Összefoglalás

A pattern matching a Rust egyik legjobban megtérülő beruházása: ha megtanulod a match, if let, while let, guard feltételek és @ kötések kombinálását, a kódod egyszerre lesz tömörebb és biztonságosabb. A kimerítő mintaillesztés miatt a fordító gyakorlatilag garantálja, hogy nem hagysz ki egy esetet sem – ez a fajta biztonság ritka más nyelvekben. Kezdetben talán furcsa lesz az @ szintaxis vagy a guard feltételek keverése destrukturálással, de néhány hét gyakorlás után ezek annyira természetessé válnak, hogy hiányolni fogod őket, amikor másik nyelven kell kódot írnod.