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

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));
}
Figyelem

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)] a thiserror makrója, amely implementálja helyettünk a std::error::Error trait-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 a Display implementációt – ez adja a {e} formázáshoz a szöveget a println!-ben.
  • A Debug-ot a thiserror nem generálja le neked, azt külön fel kell venned a derive listába – ezért írtunk #[derive(Debug, Error)]-t.
  • A #[from] attribútum egy From<std::num::ParseIntError> for ConfigError implementá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.
Megjegyzés

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/Option kombinátorai (map, map_err, unwrap_or_else, filter, ok_or_else stb.) sokkal olvashatóbbá teszik a hibakezelést, mint a beágyazott match-ek.
  • A ? operátor a hibák propagálására szolgál, és a From trait-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ára Result-ot adj vissza.
  • A thiserror crate 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: