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