Az előző leckében a lifetime annotációkkal biztosítottuk, hogy a referenciáink ne mutassanak érvénytelen adatra. Most egy másik, ugyanolyan fontos területre lépünk: mi történik, ha egy függvény nem tud eredményt adni? A Rust nem dob kivételt (exception) úgy, mint sok más nyelv – helyette a hibát a típusrendszer részévé teszi. Ez elsőre több gépelésnek tűnik, de cserébe a fordító kikényszeríti, hogy minden hibaesetet kezelj. Ez az a pont, ahol a kódod elkezd valóban „rustosan” viselkedni.
Result és Option mélyebb áttekintése
Az Option<T>-t és a Result<T, E>-t már ismered alapszinten match-elhető enumként. A gyakorlatban azonban ritkán írunk minden esetre külön match-et – helyette kombinátorokat használunk, amelyek láncolhatók, és csak akkor „ágaznak el”, amikor tényleg szükséges.
fn parse_positive(input: &str) -> Option<u32> {
input
.parse::<i32>()
.ok() // Result -> Option, a hibát elfelejtjük
.filter(|&n| n > 0) // csak a pozitív értékeket engedjük át
.map(|n| n as u32) // átalakítjuk a végleges típusra
}
fn main() {
let values = ["42", "-5", "nem szám", "7"];
for value in values {
match parse_positive(value) {
Some(n) => println!("{value} -> érvényes pozitív szám: {n}"),
None => println!("{value} -> nem érvényes"),
}
}
}
A Result<T, E>-nél hasonlóan hasznos a map (a sikeres ág átalakítására), a map_err (a hibaág átalakítására) és az unwrap_or_else (alapérték adása egy hiba esetén).
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("nullával nem lehet osztani".to_string())
} else {
Ok(a / b)
}
}
fn main() {
let result = divide(10.0, 0.0)
.map(|n| n * 2.0)
.unwrap_or_else(|err| {
println!("Hiba történt: {err}");
0.0
});
println!("Eredmény: {result}");
}
Ha csak arra van szükséged, hogy egy Result-ból kinyerj egy alapértéket anélkül, hogy a hibát is felhasználnád, elég az unwrap_or(alapérték) – az unwrap_or_else csak akkor kell, ha a hiba alapján számolod ki az alapértéket, vagy mellékhatást (pl. logolást) is végzel.
A ? operátor: hibapropagálás elegánsan
Ha egy függvényben több lépés is hibázhat, a sok egymásba ágyazott match gyorsan olvashatatlanná válik. Erre való a ? operátor: ha az érték Ok/Some, kicsomagolja; ha Err/None, azonnal visszatér a hívó függvényből ugyanazzal a hibával.
use std::num::ParseIntError;
fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {
let n: i32 = input.parse()?; // ha hiba, itt azonnal visszatérünk
Ok(n * 2)
}
fn main() {
match parse_and_double("21") {
Ok(n) => println!("Dupla: {n}"),
Err(e) => println!("Hiba: {e}"),
}
}
A ? a kulisszák mögött a From trait-et használja: ha a hívott függvény hibatípusa különbözik a jelenlegi függvény hibatípusától, a ? automatikusan konvertál, feltéve hogy létezik megfelelő From implementáció. Ez az, ami lehetővé teszi, hogy egyetlen egyéni hibatípusba "gyűjtsük össze" a különböző forrásból jövő hibákat – ezt a thiserror résznél fogjuk kihasználni.
Fontos szabály: a ? csak olyan függvényben használható, amelynek visszatérési típusa Result (vagy Option). Nézzük meg, mit üzen a fordító, ha ezt megsértjük:
fn parse_and_double(input: &str) -> i32 {
let n: i32 = input.parse()?; // hiba: a függvény nem Result-ot ad vissza
n * 2
}
fn main() {
println!("{}", parse_and_double("21"));
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:2:20
|
1 | fn parse_and_double(input: &str) -> i32 {
| ---------------------------------------- this function should return `Result` or `Option` to accept `?`
2 | let n: i32 = input.parse()?;
| ------------- this expression has type `Result<i32, ParseIntError>`
| ^ cannot use the `?` operator in a function that returns `i32`
|
= help: the trait `FromResidual<Result<Infallible, ParseIntError>>` is not implemented for `i32`
panic! vs Result: mikor melyiket?
A Rustban két alapvetően különböző eszközöd van hibák jelzésére. A panic! egy azonnali, visszafordíthatatlan leállást okoz – a program (vagy a szál) megszakad. A Result viszont a hívóra bízza a döntést: ő eldöntheti, hogy próbál újat, alapértéket használ, vagy továbbadja a hibát.
Ökölszabályként:
panic!-ot akkor használj, ha egy programozói hiba vagy egy megsérült invariáns miatt a program állapota már nem megbízható, és a folytatás veszélyes lenne.Result-ot használj minden olyan esetben, amikor a hiba várható, kezelhető – pl. rossz felhasználói input, hiányzó fájl, sikertelen hálózati hívás (ezt egy későbbi leckében részletesen is látni fogod).
fn get_element(items: &[i32], index: usize) -> Option<i32> {
// a .get() sosem panickol, hanem None-t ad vissza érvénytelen indexre
items.get(index).copied()
}
fn set_percentage(value: f64) -> f64 {
// ez viszont programozói hiba lenne, ha előfordulna – itt jogos a panic!
if !(0.0..=100.0).contains(&value) {
panic!("a százaléknak 0 és 100 között kell lennie, kaptunk: {value}");
}
value
}
fn main() {
let numbers = vec![1, 2, 3];
println!("{:?}", get_element(&numbers, 10)); // None, nincs panic
println!("{}", set_percentage(87.5));
}
A numbers[10] indexelés (a [] operátor) panickolna, ha az index túl nagy – ezért könyvtári kódban, ahol nem tudod, milyen input érkezik, inkább a .get()-et használd, és a hívóra bízd a hiba kezelését.
Egyéni hibatípusok a thiserror crate-tel
A String mint hibatípus (amit fentebb a divide függvénynél láttunk) rövid programokra elég, de egy nagyobb kódbázisban hamar követhetetlenné válik: nem lehet rá match-elni, és elveszik minden strukturált információ. Ehelyett saját enum hibatípust szoktunk definiálni – ehhez nyújt kényelmes segítséget a thiserror crate.
[dependencies]
thiserror = "2"
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("hiányzó mező: {0}")]
MissingField(String),
#[error("érvénytelen érték a '{field}' mezőben: {value}")]
InvalidValue { field: String, value: String },
#[error("nem sikerült feldolgozni a számértéket")]
ParseFailed(#[from] std::num::ParseIntError),
}
fn load_timeout(raw: Option<&str>) -> Result<u32, ConfigError> {
let raw = raw.ok_or_else(|| ConfigError::MissingField("timeout".to_string()))?;
let value: u32 = raw.parse()?; // a #[from] miatt automatikusan ConfigError-rá alakul
Ok(value)
}
fn main() {
match load_timeout(Some("30")) {
Ok(timeout) => println!("Timeout beállítva: {timeout} másodperc"),
Err(e) => println!("Konfigurációs hiba: {e}"),
}
match load_timeout(None) {
Ok(timeout) => println!("Timeout beállítva: {timeout} másodperc"),
Err(e) => println!("Konfigurációs hiba: {e}"),
}
}
Mi történik itt pontosan?
- A
#[derive(Error)]athiserrormakrója, amely implementálja helyettünk astd::error::Errortrait-et (aminek a létezését már ismered), így a típusunk „igazi” hibaként viselkedik. - Minden variánshoz tartozó
#[error("...")]string generálja aDisplayimplementációt – ez adja a{e}formázáshoz a szöveget aprintln!-ben. - A
Debug-ot a thiserror nem generálja le neked, azt külön fel kell venned aderivelistába – ezért írtunk#[derive(Debug, Error)]-t. - A
#[from]attribútum egyFrom<std::num::ParseIntError> for ConfigErrorimplementációt hoz létre, ami pontosan az, amire a?operátornak szüksége van ahhoz, hogy automatikusan konvertáljon a két hibatípus között.
A thiserror a saját könyvtáradban/binárisodban definiált hibatípusokhoz ideális. Ha egy alkalmazás legfelső szintjén sokféle hibát szeretnél egyetlen „bármi lehet” típusba gyűjteni, arra a anyhow crate a bevált eszköz – ezt egy másik alkalommal érdemes megnézni.
Próbáld ki!
Vegyél elő a fenti ConfigError enumot, és bővítsd egy új OutOfRange { min: u16, max: u16, got: u16 } variánssal. Írj hozzá egy load_port(raw: Option<&str>) -> Result<u16, ConfigError> függvényt, amely a ? operátorral hívja a parse-ot, majd ellenőrzi, hogy az érték 1 és 65535 közé esik-e – ha nem, adja vissza az OutOfRange hibát. Próbáld ki több bemenettel is (None, "abc", "0", "8080"), és nézd meg, minden ág a helyes hibaüzenetet írja-e ki.
Önellenőrzés
1. Mikor válasszunk panic!-ot, és mikor Result-ot egy hiba jelzésére?
Megoldás
panic!-ot akkor, ha a hiba egy programozói hibát vagy megsérült invariánst jelez, és a program állapota már nem megbízható – ilyenkor a folytatás veszélyesebb lenne, mint a leállás. Result-ot akkor, ha a hiba várható és kezelhető – például rossz felhasználói input, hiányzó fájl vagy sikertelen művelet –, és a hívó fél számára hasznos, hogy eldönthesse, mit tegyen vele.
2. Mi történik pontosan, amikor egy Result-ra ?-t alkalmazunk egy Ok értéken, illetve egy Err értéken?
Megoldás
Ok(x) esetén a ? kicsomagolja az x értéket, és a kifejezés eredménye x lesz, a végrehajtás pedig folytatódik. Err(e) esetén a ? azonnal visszatér a jelenlegi függvényből egy Err-rel – szükség esetén a From trait segítségével átkonvertálva e-t a függvény saját hibatípusára –, így a hívó lánc további tagjai már nem futnak le.
3. Miért érdemes a #[from] attribútumot használni egy thiserror-os enum variánsán, ha ?-t szeretnénk használni a hívó kódban?
Megoldás
A #[from] automatikusan legenerál egy From<KülsőHibaTípus> for SajátHibaEnum implementációt. A ? operátor pontosan ezt a From konverziót hívja meg a kulisszák mögött, amikor a hívott függvény hibatípusa eltér a jelenlegi függvényétől. Ha a #[from]-ot nem írjuk oda, a ? nem fog fordulni a két típus között, mert nincs meg a szükséges konverzió.
Összefoglaló és mi jön legközelebb
- A
Result/Optionkombinátorai (map,map_err,unwrap_or_else,filter,ok_or_elsestb.) sokkal olvashatóbbá teszik a hibakezelést, mint a beágyazottmatch-ek. - A
?operátor a hibák propagálására szolgál, és aFromtrait-en keresztül automatikusan konvertál a hívott és a hívó függvény hibatípusai között. panic!-ot csak programozói hibák és megsérült invariánsok jelzésére használj – minden várható, kezelhető hibáraResult-ot adj vissza.- A
thiserrorcrate segítségével gyorsan tudsz strukturált,match-elhető, szövegesen is formázott hibatípusokat definiálni,#[error("...")]és#[from]attribútumokkal. - A most tanult eszközök (
?, egyéni hibaenumok, kombinátorok) együtt adják a Rust idiomatikus hibakezelési stílusát – ezt fogod a következő leckében, a „Hibakezelés-projekt”-ben egy összefüggő, több modulból álló programon keresztül gyakorlatba is átvinni.
További olvasmányok: