Cél és előfeltétel
A builder ellenőrzései már megmutatták, hogy egy sikertelen létrehozás természetes kimenet lehet. Most olyan könyvtári hibát tervezel, amelyet a hívó program elágazáshoz és emberi magyarázathoz is használhat. Az előfeltétel a Result, a saját enum és a match ismerete. A cél három réteg következetes szétválasztása: mi romlott el a tartományban, melyik művelet közben történt, és maradt-e mögötte másik konkrét hiba.
A megjelenítés és a vizsgálhatóság külön feladat
A Debug fejlesztői betekintést ad az értékbe; a Display rövid, érthető szöveget készít. Egy publikus hibatípus rendszerint mindkettőt megvalósítja. A std::error::Error ezekre épít, és a source metódusával egy esetleges mögöttes hibára mutathat. Nem minden hibának van forrása: egy tiltott nulla mennyiség önmagában is teljes magyarázat, míg egy számolvasási hibát érdemes eredeti ParseIntError értékként megőrizni.
A forráslánc nem a függvényhívások automatikus története. Csak azokat az összefüggéseket tartalmazza, amelyeket a hibatípus tervezője eltárol és visszaad. Az üzenetbe másolt eredeti hibaszöveg nem helyettesíti ezt, mert utána a hívó már csak karaktereket lát. A típusos tárolás lehetővé teszi, hogy a naplózó a külső összefüggést és a belső okot külön sorban jelenítse meg.
A hibák határát a javítás lehetősége alapján válaszd meg. Ha a hívó eltérően kezeli a hiányzó adatot és a rossz tartományt, legyenek külön változatok. Ha mindig ugyanaz a teendő, néha elegendő egy közös kategória. A túl részletes belső szerkezet ugyanúgy megnehezíti a felület használatát, mint az összes okot elrejtő általános „sikertelen” szöveg.
Egy könyvtár és egy parancssori program hibakezelése eltérő szerepet kap. A könyvtár általában visszaadja a hibát; a program dönt a megjelenítésről és a kilépési állapotról. Ha a könyvtári függvény maga ír a terminálra, a hívó nehezebben tudja ugyanazt a műveletet más felületen használni. A példák ezért tiszta visszatérési értékeket adnak, a tesztek pedig közvetlenül ezeket vizsgálják. Nincs rejtett naplózás, és a hibakezelés nem támaszkodik arra, hogy valaki észrevesz egy képernyőre írt mondatot.
A forráslánc megőrzése mellett az új kontextusnak is legyen hozzáadott értéke. Egy „nem sikerült” felirat nem segít eldönteni, melyik adat javítandó. Egy mennyiségre utaló külső üzenet már eligazít, miközben a konkrét parserhiba megmarad alatta. A tesztekben ezért érdemes eltérő kérdéseket feltenni a külső és a belső szintnek. Az egyik a művelet jelentését ellenőrzi, a másik a technikai ok megőrzését. Így egy későbbi szövegezési változtatás nem mossa össze az adatmodell és a megjelenítés felelősségét.
1. feladat: önálló tartományhiba
Készíts hibát a nulla mennyiséghez. A megjelenítése legyen pontosan az ellenőrzött mondat, a forrása pedig hiányozzon. Itt nem történt más sikertelen művelet, ezért egy kitalált alsóbb szintű ok félrevezető lenne. A két állítást külön teszteld: a jó üzenet önmagában még nem igazolja a forráslánc helyességét.
Megoldás
use std::{error::Error, fmt};
#[derive(Debug)]
struct ZeroQuantity;
impl fmt::Display for ZeroQuantity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "a mennyiség nem lehet nulla")
}
}
impl Error for ZeroQuantity {}
fn main() {
let error = ZeroQuantity;
assert_eq!(error.to_string(), "a mennyiség nem lehet nulla");
assert!(error.source().is_none());
}
Forrást kölcsönzünk, nem új hibát készítünk
A source visszatérési értéke egy kölcsönzött trait objektum. A mögöttes érték a külső hibában marad; a hívó csak megvizsgálja. A 'static megkötés ezen a trait objektumon nem azt jelenti, hogy a helyi hibaváltozót örökké tárolni kell. A tárolt hibatípus nem támaszkodhat rövidebb életű, kívülről kölcsönzött adatra. A szabványos egészszám-olvasási hiba megfelel ennek.
2. feladat: eredeti számolvasási ok
Csomagold be a ParseIntError értéket, és a source metódusból magára a tárolt hibára adj hivatkozást. Egy hibás szövegnél ellenőrizd, hogy a forrás a megfelelő konkrét típusra visszaalakítható. Az üzenet pontos szövegét most nem kötjük a standard könyvtár belső megfogalmazásához; csak a saját külső mondatot rögzítjük.
Megoldás
use std::{error::Error, fmt, num::ParseIntError};
#[derive(Debug)]
struct NumberError(ParseIntError);
impl fmt::Display for NumberError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "nem olvasható egész szám")
}
}
impl Error for NumberError {
fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.0) }
}
fn main() {
let error = NumberError("sok".parse::<u16>().unwrap_err());
assert_eq!(error.to_string(), "nem olvasható egész szám");
assert!(error.source().unwrap().downcast_ref::<ParseIntError>().is_some());
assert!("42".parse::<u16>().is_ok());
}
3. feladat: két külön javítási irány
A mennyiség olvasásánál a szintaktikai és a tartományhibát különítsd el. A megengedett tartomány egytől százig tart. A nulla jól olvasható szám, ezért ne nevezd számolvasási hibának. Az üres szöveg viszont már az első lépésben elbukik. A sikeres értéket, a két hibaváltozatot és mindkettő forrásviselkedését is ellenőrizd.
Megoldás
use std::{error::Error, fmt, num::ParseIntError};
#[derive(Debug)]
enum QuantityError { Number(ParseIntError), Range }
impl fmt::Display for QuantityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Number(_) => write!(f, "hibás mennyiségformátum"),
Self::Range => write!(f, "a mennyiség 1 és 100 közé essen"),
}
}
}
impl Error for QuantityError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self { Self::Number(e) => Some(e), Self::Range => None }
}
}
fn quantity(text: &str) -> Result<u16, QuantityError> {
let value = text.parse().map_err(QuantityError::Number)?;
if (1..=100).contains(&value) { Ok(value) } else { Err(QuantityError::Range) }
}
fn main() {
assert_eq!(quantity("12").unwrap(), 12);
let zero = quantity("0").unwrap_err();
assert!(matches!(zero, QuantityError::Range));
assert!(zero.source().is_none());
let empty = quantity("").unwrap_err();
assert!(matches!(empty, QuantityError::Number(_)));
assert!(empty.source().is_some());
assert!(matches!(quantity("101"), Err(QuantityError::Range)));
}
Elfogadási feltételek
- A megjelenítés emberi mondat, a program pedig enumváltozat alapján dönt; nem hibaszöveget keresgél.
- A számolvasási hiba megőrzi eredeti forrását, az önálló tartományhiba nem gyárt mesterséges okot.
- A nulla, az üres szöveg és a felső határt meghaladó érték külön ellenőrzött eset.
Továbblépés
A bemeneti adatot csak akkor tedd a hibaüzenetbe, ha valóban szükséges és megfelelő a célközönségnek. A példák semmilyen teljes felhasználói sort nem naplóznak. Következőként a generikus és dinamikus API-k felől vizsgáljuk meg, hogyan fogadhat egy könyvtár többféle viselkedést ugyanilyen világos szerződéssel.