Ha C++-ból, Javából vagy Pythonból jössz, valószínűleg megszoktad, hogy a hibák valahol a levegőben lebegnek, és try/catch blokkokkal kapod el őket – vagy nem, és a program elszáll. A Rust ezzel szemben egészen más filozófiát követ: a hibalehetőség a típusrendszer része, nem egy rejtett, láthatatlan kontrollfolyam.
A Result<T, E> típus és miért jobb, mint a kivételek
A Result<T, E> egy egyszerű enum:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ennyi. Nincs mágia, nincs stack unwinding a háttérben, nincs rejtett teljesítménybüntetés. Ha egy függvény visszaadhat hibát, ez a szignatúrájából azonnal látszik:
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
fn main() {
match read_file_content("config.txt") {
Ok(content) => println!("Fájl tartalma: {content}"),
Err(e) => eprintln!("Hiba történt: {e}"),
}
}
Ennek a megközelítésnek több előnye is van a kivételekhez képest:
- Explicit szerződés: a fordító megköveteli, hogy kezeld (vagy tudatosan elhanyagold) a hibát. Nincs "elfelejtett catch blokk".
- Nincs rejtett kontrollfolyam: pontosan látod a kódban, hol térhet vissza korán a függvény.
- Nulla futásidejű overhead: a
Resultegy sima enum, nincs mögötte exception-mechanizmus. - Komponálhatóság: a
Resultugyanolyan típus, mint bármi más, tehátmap-elhető,and_then-elhető, mintázható.
Ne használj unwrap()-et éles kódban csak azért, mert "biztos nem lesz hiba". A Rust compiler pontosan azért kényszerít a hibakezelésre, hogy elkerüld a futásidejű panicot – az unwrap() ezt az egész biztonsági hálót kidobja az ablakon.
A ? operátor és a hibapropagáció
A fenti read_file_content függvényben már láthattad a ? operátort. Ez a leggyakoribb módja annak, hogy egy hibát feljebb adjunk anélkül, hogy minden lépésnél explicit match-et kellene írnunk.
A ? operátor pontosan azt csinálja, mint egy match:
// Ez:
let file = File::open(path)?;
// ezzel egyenértékű:
let file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e.into()),
};
A lényeg az .into() hívásban van: a ? operátor automatikusan konvertálja a hibát a célfüggvény hibatípusára, ha van hozzá From implementáció. Ez teszi lehetővé, hogy egy alacsonyabb szintű hibát (mondjuk io::Error) egy saját, magasabb szintű hibatípusra alakítsunk propagálás közben.
A ? operátor csak akkor működik, ha a hívó függvény visszatérési típusa Result (vagy Option), és a hibatípusok között van From implementáció (vagy azonosak). Ha ez hiányzik, a fordító azonnal jelez.
Egy apró, de hasznos 2021-es kiegészítés a let-else szintaxis, amivel az early-return mintákat lehet tömörebben leírni feltételek esetén – nem hibapropagáció, de gyakran együtt használjuk:
fn first_word(input: &str) -> &str {
let Some(word) = input.split_whitespace().next() else {
return "";
};
word
}
Egyedi hibatípusok a thiserror crate-tel
Ha könyvtárat írsz, a std::io::Error vagy egy generikus String hibaüzenet nem elegáns megoldás. Sokkal jobb, ha saját, strukturált hibatípusod van, amit a hívó fél mintázhat (match-elhet).
Itt jön képbe a thiserror crate, ami a boilerplate kódot generálja neked (a Display és az Error trait implementációját):
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("nem sikerült megnyitni a fájlt: {0}")]
Io(#[from] std::io::Error),
#[error("hibás formátum a {line}. sorban")]
ParseError { line: usize },
#[error("hiányzó kulcs: {0}")]
MissingKey(String),
}
fn parse_config(raw: &str) -> Result<(), ConfigError> {
if raw.is_empty() {
return Err(ConfigError::MissingKey("db_url".to_string()));
}
Ok(())
}
A #[from] attribútum automatikusan generál egy From<std::io::Error> for ConfigError implementációt, így a ? operátor működik olyan függvényekben is, amik io::Error-t dobó hívásokat tartalmaznak, de ConfigError-t adnak visszatérési típusként.
A thiserror csak makrókat generál fordítási időben, futásidejű overhead nélkül – ezért ez az ajánlott választás library kódhoz, ahol a hívó félnek tudnia kell mintázni a konkrét hibaeseteket.
Az Cargo.toml-ba ennyi elég:
[dependencies]
thiserror = "1.0"
Gyors prototípusozás anyhow-val
Míg a thiserror a strukturált, mintázható hibákra jó, egy alkalmazás (bináris) belsejében gyakran nem érdekel, pontosan milyen hiba történt – csak az, hogy történt-e, és hogy jó legyen a hibaüzenet a logban. Erre találták ki az anyhow crate-et.
use anyhow::{bail, Context, Result};
fn load_settings(path: &str) -> Result<String> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("nem tudtam beolvasni: {path}"))?;
if content.trim().is_empty() {
bail!("a konfigurációs fájl üres: {path}");
}
Ok(content)
}
fn main() -> Result<()> {
let settings = load_settings("app.toml")?;
println!("{settings}");
Ok(())
}
Az anyhow::Result<T> valójában egy Result<T, anyhow::Error> alias, ahol az anyhow::Error bármilyen std::error::Error-t implementáló típust magába tud csomagolni. A .context() és .with_context() metódusokkal pedig egy egész hibaláncot építhetsz fel, ami hibakeresésnél aranyat ér – minden szint hozzáteszi a saját kontextusát a hibaüzenethez.
Az Cargo.toml-ban:
[dependencies]
anyhow = "1.0"
Mikor melyiket válasszuk: könyvtár vs alkalmazás kód
Ez a leggyakoribb kérdés, amit kezdők (és haladók) is feltesznek. A válasz nagyjából egy jól bevált konvenció:
- Könyvtár (library crate): használj
thiserror-t (vagy kézzel írt hibatípust). A hívó félnek joga van tudni, pontosan milyen hiba történt, és esetleg más módon reagálni rá (match ConfigError::Io(_) => ...). Egy library API-jának stabilnak és mintázhatónak kell lennie. - Alkalmazás (binary crate): használj
anyhow-t. Amainfüggvényben vagy egy CLI eszközben a legtöbbször nem kell megkülönböztetni a hibaforrásokat – elég, ha jó, kontextusban gazdag hibaüzenetet kap a felhasználó vagy a log.
// main.rs egy tipikus CLI alkalmazásban
use anyhow::Result;
fn main() -> Result<()> {
let config = load_settings("app.toml")?;
println!("Betöltve: {config}");
Ok(())
}
# fn load_settings(_: &str) -> Result<String> { Ok(String::new()) }
Sok valós projektben a kettő keveredik: a belső könyvtári modulok thiserror-alapú enumokat exportálnak, a main.rs pedig anyhow::Result-ot használ, és a ? operátor a From implementáció (amit a thiserror generál) miatt zökkenőmentesen konvertál a konkrét hibatípusból az anyhow::Error-ba.
Ha egy függvény API felszínén van (public, más crate is hívhatja), gondold át kétszer, mielőtt anyhow::Error-t adnál vissza – ezzel elveszed a hívótól a lehetőséget, hogy mintázza a konkrét hibaesetet. anyhow inkább a végfelhasználói kód (bin) belseje, nem a public API.
Összefoglalás
A Rust hibakezelése első ránézésre talán szigorúbbnak tűnik, mint a kivétel-alapú nyelvek, de ez a szigorúság a barátod: a Result<T, E> explicit, komponálható, és zéró rejtett költséggel jár. A ? operátor eltünteti a boilerplate match-eket, a thiserror strukturált, mintázható hibatípusokat ad a könyvtáraidnak, az anyhow pedig gyors, kontextusban gazdag hibakezelést biztosít az alkalmazásod belsejében. Ha ezt a három eszközt együtt használod, a hibakezelésed egyszerre lesz típusbiztos és kényelmes – pontosan az, amire szükséged van egy valódi, produkciós Rust projektben.