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.
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ó.
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:?}"),
}
}
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.
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.