Ha eddig más nyelvekből jöttél, valószínűleg a switch-hez hasonlítod a Rust match kifejezését. Ez részben igaz, de a hasonlóság megtévesztő – a match sokkal többre képes, és a fordító a háttérben komoly munkát végez, hogy garantálja: minden lehetséges esetet lefedtél.

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

A match egy kifejezés, ami azt jelenti, hogy értéket ad vissza. Ez alapvető különbség sok más nyelvhez képest, ahol a switch csak egy vezérlési szerkezet.

fn leiras(szam: i32) -> &'static str {
    match szam {
        0 => "nulla",
        1 | 2 | 3 => "kicsi",
        4..=10 => "közepes",
        _ => "nagy",
    }
}

fn main() {
    for i in [0, 2, 7, 42] {
        println!("{i}: {}", leiras(i));
    }
}

Ami igazán különlegessé teszi a match-et, az az úgynevezett exhaustiveness checking, vagyis a kimerítő ellenőrzés. A fordító megköveteli, hogy minden lehetséges érték le legyen fedve – ha kihagysz egy esetet, fordítási hibát kapsz, nem futásidejű meglepetést.

Tipp

Ha egy enum-hoz később hozzáadsz egy új variánst, a Rust azonnal jelezni fogja minden match-nél, ahol nem kezelted le az új esetet. Ez az egyik legjobb védőháló refaktoráláshoz.

Ez különösen jól látszik enumoknál:

enum Muvelet {
    Osszead(i32, i32),
    Kivon(i32, i32),
    Nullaz,
}

fn vegrehajt(m: Muvelet) -> i32 {
    match m {
        Muvelet::Osszead(a, b) => a + b,
        Muvelet::Kivon(a, b) => a - b,
        Muvelet::Nullaz => 0,
    }
}

Ha itt kikommentezed a Muvelet::Nullaz ágat, a rustc nem fog fordítani, és pontosan megmondja, melyik variánst hagytad ki. Ez a fajta biztonság az egyik oka annak, hogy a Rust kódban ritkábban futunk bele „elfelejtett eset” típusú hibákba.

Mintázatok: literálok, tartományok és guard feltételek

A match mintái (patterns) sokkal gazdagabbak, mint egy egyszerű érték-összehasonlítás. Láttuk már a literálokat (0, 1 | 2 | 3) és a tartományokat (4..=10). Fontos, hogy a tartomány mindig zárt intervallum a ..= szintaxissal – a nyílt végű .. tartomány mintaként (pattern) nem használható, csak kifejezésként.

A guard feltételek (if egy mintaág után) lehetővé teszik, hogy tetszőleges logikai feltételt is beleköss az illesztésbe:

fn kategorizal(x: i32, y: i32) -> &'static str {
    match (x, y) {
        (0, 0) => "origó",
        (x, y) if x == y => "átló",
        (x, _) if x > 0 => "jobb félsík",
        _ => "egyéb",
    }
}
Figyelem

A guard feltétel nem számít bele a kimerítőségi ellenőrzésbe! A fordító nem tudja bizonyítani, hogy egy if feltétel lefedi-e az összes esetet, ezért mindig kell egy _ vagy egy másik, guard nélküli ág, ami elkapja a maradékot.

A @ kötési operátor (binding) segítségével egyszerre illeszthetsz mintára és köthetsz nevet az értékhez:

fn ellenoriz(kor: u8) -> String {
    match kor {
        eletkor @ 0..=17 => format!("{eletkor} éves: kiskorú"),
        eletkor @ 18..=64 => format!("{eletkor} éves: felnőtt"),
        eletkor => format!("{eletkor} éves: nyugdíjas"),
    }
}

Az if let és while let egyszerűsített formák használata

Ha csak egyetlen mintára vagy kíváncsi, és a többi esettel nem akarsz foglalkozni, felesleges lenne egy teljes match-et felépíteni. Erre való az if let:

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

A while let ugyanezt teszi ciklusban – addig fut, amíg a mintaillesztés sikeres:

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

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

Ez kifejezetten hasznos, ha valamilyen iterátor-szerű struktúrát dolgozol fel, amíg az Option vagy Result sikeres eredményt ad vissza.

A Rust 1.65 óta emellett rendelkezésünkre áll a let-else szintaxis is, ami az if let-nek egyfajta "fordítottja": ha a minta nem illeszkedik, korai kilépést (early return, continue, break vagy panic!) kényszerít ki.

fn feldolgoz_kotelezo(opt: Option<i32>) -> i32 {
    let Some(ertek) = opt else {
        println!("Nincs érték, alapértelmezett visszatérés");
        return -1;
    };

    ertek * 2
}
Megjegyzés

A let-else nagyszerűen csökkenti a beágyazott if let blokkok mélységét, ha a "boldog út" (happy path) folytatásához feltétlenül szükséges egy sikeres mintaillesztés.

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

A match (és az if let, let-else is) nem csak primitív típusokra, hanem tetszőlegesen összetett adatszerkezetekre is illeszthető. Struct-oknál a mezőneveket használjuk:

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

fn hol_van(p: Pont) -> &'static str {
    match p {
        Pont { x: 0, y: 0 } => "origó",
        Pont { x, y: 0 } => {
            println!("x tengelyen, x = {x}");
            "x tengely"
        }
        Pont { x: 0, y } => {
            println!("y tengelyen, y = {y}");
            "y tengely"
        }
        Pont { .. } => "egyéb",
    }
}

A .. szintaxis lehetővé teszi, hogy a fennmaradó mezőket figyelmen kívül hagyd – ezt nem szabad összekeverni a tartomány ..=-szal, bár mindkettő a "nem érdekel a részlet" gondolatát fejezi ki.

Beágyazott enumoknál a destrukturálás igazán kifizetődik:

enum Alak {
    Kor { sugar: f64 },
    Teglalap { szelesseg: f64, magassag: f64 },
}

fn terulet(alak: &Alak) -> f64 {
    match alak {
        Alak::Kor { sugar } => std::f64::consts::PI * sugar * sugar,
        Alak::Teglalap { szelesseg, magassag } => szelesseg * magassag,
    }
}

Fontos, hogy referencián (&Alak) illesztve a mezők automatikusan referenciaként köződnek – ezt hívjuk match ergonomics-nak, ami jelentősen csökkenti a szükséges &/* operátorok számát a mintákban.

Gyakorlati példa: egyszerű állapotgép megvalósítása mintaillesztéssel

Nézzünk egy valós felhasználási esetet: egy egyszerű forgalomlámpa-állapotgépet, ahol az állapotátmeneteket kizárólag match-csel írjuk le. Ez a minta rendkívül elterjedt Rust kódban, mert az enum + match kombináció statikusan garantálja, hogy nem maradhat kezeletlen átmenet.

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

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

fn kovetkezo_allapot(jelenlegi: Lampa, esemeny: Esemeny) -> Lampa {
    match (jelenlegi, esemeny) {
        (Lampa::Piros, Esemeny::Idozito) => Lampa::Zold,
        (Lampa::Zold, Esemeny::Idozito) => Lampa::Sarga,
        (Lampa::Sarga, Esemeny::Idozito) => Lampa::Piros,
        // Vészhelyzetben mindig pirosra vált, függetlenül a jelenlegi állapottól
        (_, Esemeny::Vesz) => Lampa::Piros,
    }
}

fn main() {
    let mut allapot = Lampa::Piros;
    let esemenyek = [Esemeny::Idozito, Esemeny::Idozito, Esemeny::Vesz, Esemeny::Idozito];

    for esemeny in esemenyek {
        allapot = kovetkezo_allapot(allapot, esemeny);
        println!("Új állapot: {allapot:?}");
    }
}

Az állapotpár (jelenlegi, esemeny) tuple-ként illesztése rendkívül olvasható kódot eredményez, és mivel a match kimerítő, a fordító azonnal jelezné, ha kihagynánk egy lehetséges kombinációt – például ha az Esemeny enumhoz egy Karbantartas variánst adnánk hozzá, minden érintett match kifejezésnél hibát kapnánk, amíg le nem kezeljük.

Jó tudni

Állapotgépeknél különösen érdemes tuple-mintázatot használni (állapot, esemény) formában, mert így egyetlen helyen, egyetlen match-ben látod át az összes lehetséges átmenetet – ez önmagában is remek dokumentáció.

Összefoglalás

A match kifejezés messze túlmutat egy egyszerű elágazási szerkezeten: a kimerítő ellenőrzés, a gazdag mintanyelv (literálok, tartományok, guard-ok, @ kötések), valamint a struct- és enum-destrukturálás együtt egy rendkívül kifejező és biztonságos eszközt adnak a kezünkbe. Az if let és while let az egyszerűbb, egy mintára fókuszáló eseteket rövidíti le, míg a let-else a korai kilépéses mintákat teszi olvashatóbbá. Ha legközelebb egy enum-mal dolgozol, próbáld meg mindig match-csel, tuple-mintákkal modellezni az átmeneteket – a fordító a barátod lesz, nem az ellenséged.