Rust Fehlerbehandlung Deep Dive: Mehr als Result und Option
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Fehlerbehandlung in Rust ist nicht so einfach wie nur die Verwendung von Result
und Option
. Für Anfänger kann die Fehlerbehandlung in Rust recht unfreundlich sein. Nachdem ich mehrmals damit zu kämpfen hatte, beschloss ich, mein Wissen zu diesem Thema zu ordnen. Dieser Leitfaden besteht aus zwei Hauptteilen:
- Offizielle Methoden zur Arbeit mit
Result
- Wie man benutzerdefinierte Fehler definiert und behandelt.
Das Beherrschen dieser Konzepte wird Ihnen helfen, die Herausforderungen der Fehlerbehandlung von Rust zu meistern.
Methoden zur Fehlerbehandlung
Um Fehler effektiv zu behandeln, müssen Sie die integrierten Methoden von Rust nutzen. Dies wird Ihre Arbeit erheblich erleichtern.
Einige nützliche Methoden sind:
or()
and()
or_else()
and_then()
map()
map_err()
map_or()
map_or_else()
ok_or()
ok_or_else()
- ...
Im Folgenden werde ich erklären, wann Sie diese Methoden verwenden sollten, wie Sie sie verwenden und letztendlich, wie Sie Ihre Err
-Typen beim Schreiben von Code entwerfen.
or()
und and()
Mit diesen Methoden können Sie zwischen zwei Optionen wählen, ähnlich wie bei logischem ODER und UND.
or()
: Wertet Ausdrücke der Reihe nach aus. Wenn ein Ausdruck zuSome
oderOk
führt, wird dieser Wert sofort zurückgegeben.and()
: Gibt den Wert aus dem zweiten Ausdruck zurück, wenn beideSome
oderOk
sind. Wenn eines der ErgebnisseNone
oderErr
ist, wird stattdessen dieses zurückgegeben.
let s1 = Some("some1"); let s2 = Some("some2"); let n: Option<&str> = None; let o1: Result<&str, &str> = Ok("ok1"); let o2: Result<&str, &str> = Ok("ok2"); let e1: Result<&str, &str> = Err("error1"); let e2: Result<&str, &str> = Err("error2"); assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1 assert_eq!(s1.or(n), s1); // Some or None = Some assert_eq!(n.or(s1), s1); // None or Some = Some assert_eq!(n.or(n), n); // None1 or None2 = None2 assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1 assert_eq!(o1.or(e1), o1); // Ok or Err = Ok assert_eq!(e1.or(o1), o1); // Err or Ok = Ok assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2 assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2 assert_eq!(s1.and(n), n); // Some and None = None assert_eq!(n.and(s1), n); // None and Some = None assert_eq!(n.and(n), n); // None1 and None2 = None1 assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2 assert_eq!(o1.and(e1), e1); // Ok and Err = Err assert_eq!(e1.and(o1), e1); // Err and Ok = Err assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1
or_else()
und and_then()
Die Methoden or()
und and()
wählen nur zwischen zwei Werten aus, ohne sie zu ändern. Wenn Sie komplexere Logik anwenden müssen, sollten Sie Closures mit or_else()
und and_then()
verwenden.
// Verwenden von or_else() mit Option let s1 = Some("some1"); let s2 = Some("some2"); let fn_some = || Some("some2"); // Equivalent to: let fn_some = || -> Option<&str> { Some("some2") }; let n: Option<&str> = None; let fn_none = || None; assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1 assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2 // Verwenden von or_else() mit Result let o1: Result<&str, &str> = Ok("ok1"); let o2: Result<&str, &str> = Ok("ok2"); let fn_ok = |_| Ok("ok2"); let e1: Result<&str, &str> = Err("error1"); let e2: Result<&str, &str> = Err("error2"); let fn_err = |_| Err("error2"); assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1 assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2
map()
Wenn Sie den Wert innerhalb von Result
oder Option
ändern möchten, verwenden Sie map()
.
let s1 = Some("abcde"); let s2 = Some(5); let n1: Option<&str> = None; let n2: Option<usize> = None; let o1: Result<&str, &str> = Ok("abcde"); let o2: Result<usize, &str> = Ok(5); let e1: Result<&str, &str> = Err("abcde"); let e2: Result<usize, &str> = Err("abcde"); let fn_character_count = |s: &str| s.chars().count(); assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2 assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2 assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2
map_err()
Wenn Sie den Err
-Wert in Result
ändern müssen, verwenden Sie map_err()
.
let o1: Result<&str, &str> = Ok("abcde"); let o2: Result<&str, isize> = Ok("abcde"); let e1: Result<&str, &str> = Err("404"); let e2: Result<&str, isize> = Err(404); let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2
map_or()
Wenn Sie sicher sind, dass es kein Err
geben wird, können Sie map_or()
verwenden, um stattdessen einen Standardwert zurückzugeben.
const V_DEFAULT: u32 = 1; let s: Result<u32, ()> = Ok(10); let n: Option<u32> = None; let fn_closure = |v: u32| v + 2; assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12); assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);
map_or_else()
map_or()
erlaubt nur die Rückgabe eines Standardwertes, aber wenn Sie eine Closure benötigen, um den Standardwert bereitzustellen, verwenden Sie map_or_else()
.
let s = Some(10); let n: Option<i8> = None; let fn_closure = |v: i8| v + 2; let fn_default = || 1; assert_eq!(s.map_or_else(fn_default, fn_closure), 12); assert_eq!(n.map_or_else(fn_default, fn_closure), 1);
ok_or()
Wenn Sie eine Option
in ein Result
konvertieren möchten, können Sie ok_or()
verwenden.
const ERR_DEFAULT: &str = "error message"; let s = Some("abcde"); let n: Option<&str> = None; let o: Result<&str, &str> = Ok("abcde"); let e: Result<&str, &str> = Err(ERR_DEFAULT); assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T) assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)
ok_or_else()
Wenn Sie mit potenziellen Err
-Fällen zu tun haben und einen Fehler des gleichen Typs mithilfe einer Closure zurückgeben möchten, verwenden Sie ok_or_else()
.
let s = Some("abcde"); let n: Option<&str> = None; let fn_err_message = || "error message"; let o: Result<&str, &str> = Ok("abcde"); let e: Result<&str, &str> = Err("error message"); assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T) assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)
Wie man Fehler entwirft
Anfänger sind oft frustriert über die strengen Fehlertypen von Rust, insbesondere wenn sie mit Typkonflikten bei mehreren Result
-Rückgaben konfrontiert sind. Indem Sie Result
-Typen besser verstehen, können Sie diese Frustrationen vermeiden.
Definieren einfacher benutzerdefinierter Fehler
Beim Schreiben von Programmen ist es üblich, benutzerdefinierte Fehler zu definieren. Hier ist ein Beispiel für ein einfaches benutzerdefiniertes Result
:
use std::fmt; // CustomError ist ein benutzerdefinierter Fehlertyp. #[derive(Debug)] struct CustomError; // Implementieren des Display-Traits für benutzerseitige Fehlermeldungen. impl fmt::Display für CustomError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut!") } } // Beispiel für eine Funktion, die einen CustomError generiert. fn make_error() -> Result<(), CustomError> { Err(CustomError) } fn main(){ match make_error() { Err(e) => eprintln!("{}", e), _ => println!("Kein Fehler"), } eprintln!("{:?}", make_error()); }
Hinweis: Das Makro
eprintln!
wird für die Fehlerausgabe verwendet, funktioniert aber genauso wieprintln!
, es sei denn, die Ausgabe wird umgeleitet.
Durch die Implementierung von Debug
und Display
können Sie Fehler nicht nur zur Anzeige formatieren, sondern auch benutzerdefinierte Fehler in Box<dyn std::error::Error>
-Trait-Objekte konvertieren.
Definieren komplexerer Fehler
In realen Szenarien weisen wir oft Fehlercodes und -meldungen zu:
use std::fmt; struct CustomError { code: usize, message: String, } // Anzeigen verschiedener Fehlermeldungen basierend auf dem Code. impl fmt::Display für CustomError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err_msg = match self.code { 404 => "Entschuldigung, die Seite kann nicht gefunden werden!", _ => "Entschuldigung, etwas ist schief gelaufen! Bitte versuchen Sie es erneut!", }; write!(f, "{}", err_msg) } } impl fmt::Debug für CustomError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "CustomError {{ code: {}, message: {} }}", self.code, self.message ) } } fn make_error() -> Result<(), CustomError> { Err(CustomError { code: 404, message: String::from("Seite nicht gefunden"), }) } fn main() { match make_error() { Err(e) => eprintln!("{}", e), _ => println!("Kein Fehler"), } eprintln!("{:?}", make_error()); }
Die manuelle Implementierung von
Display
undDebug
ermöglicht eine stärker angepasste Ausgabe als die Verwendung von#[derive(Debug)]
.
Fehlerkonvertierung
Was ist, wenn Sie Bibliotheken von Drittanbietern verwenden, die jeweils ihre eigenen Fehlertypen definieren? Rust bietet den std::convert::From
-Trait zur Fehlerkonvertierung.
use std::fs::File; use std::io; #[derive(Debug)] struct CustomError { kind: String, message: String, } // Konvertieren von `io::Error` in `CustomError`. impl From<io::Error> for CustomError { fn from(error: io::Error) -> Self { CustomError { kind: String::from("io"), message: error.to_string(), } } } fn main() -> Result<(), CustomError> { let _file = File::open("nonexistent_file.txt")?; Ok(()) }
Der Operator ?
konvertiert std::io::Error
automatisch in CustomError
. Dieser Ansatz vereinfacht die Fehlerbehandlung erheblich.
Behandeln mehrerer Fehlertypen
Was ist, wenn Ihre Funktion mit mehreren Fehlertypen zu tun hat?
use std::fs::File; use std::io::{self, Read}; use std::num; #[derive(Debug)] struct CustomError { kind: String, message: String, } impl From<io::Error> for CustomError { fn from(error: io::Error) -> Self { CustomError { kind: String::from("io"), message: error.to_string(), } } } impl From<num::ParseIntError> for CustomError { fn from(error: num::ParseIntError) -> Self { CustomError { kind: String::from("parse"), message: error.to_string(), } } } fn main() -> Result<(), CustomError> { let mut file = File::open("hello_world.txt")?; let mut content = String::new(); file.read_to_string(&mut content)?; let _number: usize = content.parse()?; Ok(()) }
Erweiterte Strategien zur Fehlerbehandlung
Wenn Funktionen verschiedene Fehlertypen zurückgeben, gibt es vier gängige Ansätze:
1. Verwenden von Box<dyn Error>
Diese Methode konvertiert alle Fehlertypen in ein Trait-Objekt.
use std::fs::read_to_string; use std::error::Error; fn main() -> Result<(), Box<dyn Error>> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String, Box<dyn Error>> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
Vorteile: Vereinfacht den Code. Nachteile: Geringfügiger Leistungsverlust und potenzieller Verlust von Fehlerartinformationen.
2. Benutzerdefinierte Fehlertypen
Definieren Sie ein enum
, um alle Fehlertypen darzustellen.
#[derive(Debug)] enum MyError { EnvironmentVariableNotFound, IOError(std::io::Error), } impl From<std::env::VarError> for MyError { fn from(_: std::env::VarError) -> Self { Self::EnvironmentVariableNotFound } } impl From<std::io::Error> for MyError { fn from(value: std::io::Error) -> Self { Self::IOError(value) } }
Nachteile: Ausführlich, bietet aber präzise Fehlerkontrolle.
3. Verwenden von thiserror
Vereinfacht benutzerdefinierte Fehlerdefinitionen mit Anmerkungen.
#[derive(thiserror::Error, Debug)] enum MyError { #[error("Umgebungsvariable nicht gefunden")] EnvironmentVariableNotFound(#[from] std::env::VarError), #[error(transparent)] IOError(#[from] std::io::Error), }
Sehr empfehlenswert für ein Gleichgewicht zwischen Einfachheit und Kontrolle.
4. Verwenden von anyhow
Kapselt jeden Fehlertyp und bietet Komfort auf Kosten der Leistung.
use anyhow::Result; fn main() -> Result<()> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
Abschliessende Gedanken: Einfach machen!
Mit diesen Techniken sind Sie nun in der Lage, das Fehlermanagement in Rust zu handhaben. Egal, ob Sie Einfachheit oder feine Kontrolle bevorzugen, die Fehlerbehandlungsmechanismen von Rust können sich an Ihre Bedürfnisse anpassen.
Wir sind Leapcell, Ihre erste Wahl für das Hosting von Rust-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein operativer Aufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ