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.
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",
}
}
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
}
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.
Á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.