Eigene Fehlertypen in Rust
Auf der Seite zu Result war jeder Fehler einfach ein String. Für kleine Beispiele ist das in Ordnung. Sobald eine Funktion aber auf mehr als eine Art fehlschlagen kann, stößt das an seine Grenzen. Auf einen String kannst du nicht mit match reagieren. Aufrufer können also nicht erkennen, welche Art von Fehler passiert ist – es sei denn, sie werten den Text selbst aus.
Das Problem mit String-Fehlern
Abschnitt betitelt „Das Problem mit String-Fehlern“fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("nicht genug Guthaben")); } if amount < 0.0 { return Err(String::from("der Betrag darf nicht negativ sein")); } Ok(balance - amount)}Will ein Aufrufer auf „nicht genug Guthaben“ anders reagieren als auf „negativer Betrag“, bleibt ihm nur, den Fehlertext selbst zu vergleichen. Das ist fehleranfällig und geht kaputt, sobald du eine Meldung anders formulierst.
Einen Fehler als Enum definieren
Abschnitt betitelt „Einen Fehler als Enum definieren“Ein Enum löst das Problem. Jede Art von Fehlschlag wird zu einer eigenen Variante, auf die Aufrufer direkt mit match reagieren können:
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("Neuer Kontostand: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("Nicht genug Geld"), Err(WithdrawError::NegativeAmount) => println!("Ein negativer Betrag kann nicht abgehoben werden"), }}Ausgabe:
Nicht genug GeldJetzt sorgt der Compiler dafür, dass jede Art von Fehlschlag behandelt wird. Das ist dieselbe Garantie, die dir match bei Enums schon immer gibt – nur eben für Fehler.
Display implementieren für eine ordentliche Fehlermeldung
Abschnitt betitelt „Display implementieren für eine ordentliche Fehlermeldung“Versuchst du jetzt, WithdrawError mit {} auszugeben, lässt sich das nicht kompilieren. Rust weiß nicht, wie es daraus Text machen soll. Die Lösung ist, den Trait Display zu implementieren. Traits bekommen bald eigene Seiten (Traits und Wichtige Traits). Für den Moment kannst du dieses Muster einfach so übernehmen:
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "nicht genug Guthaben"), WithdrawError::NegativeAmount => write!(f, "der Betrag darf nicht negativ sein"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Fehler: {error}");}Ausgabe:
Fehler: nicht genug GuthabenJetzt verhält sich WithdrawError wie jeder andere Fehler, den du einem Benutzer zeigen oder in ein Log schreiben möchtest. Gleichzeitig kann Code, der auf einen bestimmten Fall reagieren muss, weiterhin mit match darauf prüfen.
Wenn ein Crate helfen kann
Abschnitt betitelt „Wenn ein Crate helfen kann“In einem echten Projekt wird es schnell eintönig, Display und den übrigen Boilerplate-Code für jeden eigenen Fehlertyp von Hand zu schreiben. Das Crate thiserror (auf Englisch) erzeugt das meiste davon für dich mit einem #[derive]. anyhow (auf Englisch) ist beliebt für Anwendungscode, der Fehler nur weiterreichen muss, ohne für jeden einen eigenen Typ zu definieren. Gut zu wissen, dass es sie gibt – für die Grundlagen brauchst du sie aber nicht. Wenn du so weit bist, zeigt dir die Seite anyhow und thiserror beide in Aktion, und wann du welches nimmst.
Übungen
Abschnitt betitelt „Übungen“1. Fehler beim Login
Abschnitt betitelt „1. Fehler beim Login“Definiere ein Enum LoginError mit zwei Varianten, EmptyUsername und PasswordTooShort, und implementiere Display dafür. Schreib dann fn check_login(user: &str, password: &str) -> Result<String, LoginError>. Die Funktion gibt Ok mit einer Begrüßung zurück, oder den passenden Fehler. Ein Passwort muss mindestens 8 Zeichen lang sein. Tipp: Mit .is_empty() und .len() auf einem &str kannst du alles prüfen, was du brauchst.
Lösung anzeigen
use std::fmt;
enum LoginError { EmptyUsername, PasswordTooShort,}
impl fmt::Display for LoginError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LoginError::EmptyUsername => write!(f, "der Benutzername darf nicht leer sein"), LoginError::PasswordTooShort => write!(f, "das Passwort muss mindestens 8 Zeichen lang sein"), } }}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> { if user.is_empty() { return Err(LoginError::EmptyUsername); } if password.len() < 8 { return Err(LoginError::PasswordTooShort); } Ok(format!("Willkommen, {user}!"))}
fn main() { let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("Fehler: {e}"), } }}Ausgabe:
Willkommen, ana!Fehler: der Benutzername darf nicht leer seinFehler: das Passwort muss mindestens 8 Zeichen lang seinDie Passwörter bleiben auf Englisch, weil .len() Bytes zählt. Ein Umlaut würde zwei Bytes belegen und könnte das Ergebnis der Längenprüfung verändern.
2. Fehler mit Details
Abschnitt betitelt „2. Fehler mit Details“Schreib fn parse_percent(input: &str) -> Result<u32, PercentError>. Dabei hat PercentError zwei Varianten, die Daten enthalten: NotANumber(String) (der fehlerhafte Text) und TooLarge(u32) (die Zahl, die größer als 100 war). Sorg dafür, dass die Display-Meldungen diese Details enthalten. Tipp: Enum-Varianten können Werte enthalten, wie auf der Seite Enums. In fmt holst du den Wert mit PercentError::TooLarge(n) => ... heraus.
Lösung anzeigen
use std::fmt;
enum PercentError { NotANumber(String), TooLarge(u32),}
impl fmt::Display for PercentError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PercentError::NotANumber(text) => write!(f, "'{text}' ist keine Zahl"), PercentError::TooLarge(n) => write!(f, "{n} ist mehr als 100"), } }}
fn parse_percent(input: &str) -> Result<u32, PercentError> { match input.parse::<u32>() { Ok(n) => { if n > 100 { Err(PercentError::TooLarge(n)) } else { Ok(n) } } Err(_) => Err(PercentError::NotANumber(input.to_string())), }}
fn main() { for input in ["75", "halb", "250"] { match parse_percent(input) { Ok(n) => println!("{n}%"), Err(e) => println!("Fehler: {e}"), } }}Ausgabe:
75%Fehler: 'halb' ist keine ZahlFehler: 250 ist mehr als 100Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein
Stringals Fehler reicht für kleine Beispiele. Aufrufer können damit aber nicht gezielt auf einzelne Fehlerarten reagieren. - Definiere ein Enum mit einer Variante pro Fehlerart. Dann können Aufrufer mit
matchauf den konkreten Fehler reagieren. - Implementiere
Display(impl fmt::Display for YourError), damit sich dein Fehlertyp mit{}ausgeben lässt. - In größeren Projekten nehmen dir Crates wie
thiserrorundanyhowden meisten Boilerplate-Code ab.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Warum reicht ein String als Fehlertyp nicht mehr aus, sobald eine Funktion auf mehr als eine Art fehlschlagen kann?
2. Wodurch können Aufrufer von withdraw auf InsufficientFunds anders reagieren als auf NegativeAmount?
3. Was kannst du tun, wenn dein eigener Fehlertyp Display implementiert?
4. Wobei helfen Crates wie thiserror und anyhow?
5. Warum gilt es als fehleranfällig, rohe Fehlermeldungen als Strings zu vergleichen?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Defining an error type – Rust by Example (auf Englisch): ein kurzes, ausführbares Beispiel für einen eigenen Fehlertyp mit
Display. - Wrapping errors – Rust by Example (auf Englisch): der nächste Schritt – ein Fehler-Enum, das andere Fehler einpackt und über
Frommit?funktioniert. std::error::Error– Doku der Standardbibliothek (auf Englisch): der Standard-Trait, den „echte“ Fehlertypen implementieren, und wofür seine Methodesource()da ist.- Wann panic! aufrufen und wann nicht? – Das Rust-Buch: Richtlinien, wann du einen Fehler zurückgibst und wann du das Programm anhältst.
- thiserror – docs.rs (auf Englisch): das Crate, das
DisplayundErrorfür deine Fehler-Enums ableitet, sodass du viel weniger Code schreibst.