Zum Inhalt springen

Result in Rust

Option steht für einen Wert, der fehlen kann. Result steht für eine Operation, die fehlschlagen kann. Anders als bei einem fehlenden Wert gibt es bei einem Fehlschlag meistens auch einen Grund dafür.

Result ist ein weiteres Enum aus der Standardbibliothek. Diesmal tragen beide Varianten Daten:

enum Result<T, E> {
Ok(T), // Erfolg, mit einem Wert
Err(E), // Fehlschlag, mit einem Fehler
}

.parse() kennst du schon von der Seite Typumwandlung. Das ist ein gutes Beispiel aus der Praxis: Text in eine Zahl umzuwandeln, klappt nicht, wenn der Text gar keine Zahl ist.

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

Ausgabe:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Selbst ausprobieren

InvalidDigit bedeutet „ungültige Ziffer“: In "abc" steckt nichts, was sich als Zahl lesen lässt.

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("Umgewandelt: {number}"),
Err(e) => println!("Umwandlung fehlgeschlagen: {e}"),
}
}
▶ Selbst ausprobieren

Das ist dieselbe Form wie bei einer Option mit match. Nur liefert dir der Err-Zweig einen echten Fehlerwert, den du untersuchen oder melden kannst – nicht bloß „hier war nichts“.

fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division durch null ist nicht möglich"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Ergebnis: {result}"),
Err(e) => println!("Fehler: {e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("Ergebnis: {result}"),
Err(e) => println!("Fehler: {e}"),
}
}

Ausgabe:

Ergebnis: 5
Fehler: Division durch null ist nicht möglich
▶ Selbst ausprobieren

Weil die Funktion Result<f64, String> zurückgibt, muss sich jeder Aufrufer mit einem möglichen Fehlschlag beschäftigen. Eine Division durch null kann so nie versehentlich als normale Zahl weiterverwendet werden.

Wenn du mehrere Operationen hintereinander ausführst, die jeweils fehlschlagen können, landest du schnell bei tief verschachtelten match-Blöcken. Der ?-Operator macht das flach. Er bedeutet: „Ist das ein Err, gib diesen Fehler sofort aus der aktuellen Funktion zurück. Sonst gib mir den Wert aus dem Ok.“

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // kehrt sofort zurück, wenn die Umwandlung fehlschlägt
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("hoppla"));
}

Ausgabe:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Selbst ausprobieren

Ohne ? bräuchte diese Zeile ein eigenes match, nur um number auszupacken oder mit dem Fehler vorzeitig abzubrechen. ? erledigt genau das mit einem einzigen Zeichen. Es funktioniert allerdings nur in einer Funktion, die selbst ein Result (oder eine Option) zurückgibt. Nur dann gibt es einen Ort, an den der Fehler zurückgegeben werden kann.

Rust hat auch panic!. Das hält das Programm sofort an – mehr dazu auf der nächsten Seite. Als Faustregel gilt: Nimm Result für Fehlschläge, die ein Aufrufer sinnvollerweise behandeln möchte (falsche Eingaben, eine fehlende Datei). Heb dir panic! für Situationen auf, die auf einen echten Bug hinweisen und in denen Weitermachen schlimmer wäre als Anhalten.

Sobald deine Funktionen auf mehrere verschiedene Arten fehlschlagen können, reicht ein einfacher String als Fehler nicht mehr aus. Auf der Seite Eigene Fehlertypen lernst du, wie du deinen eigenen Fehlertyp definierst. Und anyhow und thiserror zeigt dir die Crates, mit denen die meisten Rust-Projekte sich den Boilerplate-Code sparen.

Schreib fn parse_age(input: &str) -> Result<u32, String>. Die Funktion soll ein Err mit einer hilfreichen Meldung zurückgeben, wenn der Text keine Zahl ist oder wenn die Zahl größer als 150 ist. Teste sie mit "34", "abc" und "200". Tipp: Reagiere mit match auf input.parse::<u32>(), und prüf im Ok-Zweig mit einem if die Größe.

Lösung anzeigen
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age} ist zu alt, um echt zu sein"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("'{input}' ist keine Zahl")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("Alter: {age}"),
Err(e) => println!("Fehler: {e}"),
}
}
}

Ausgabe:

Alter: 34
Fehler: 'abc' ist keine Zahl
Fehler: 200 ist zu alt, um echt zu sein
▶ Selbst ausprobieren

Schreib fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>. Die Funktion wandelt beide Strings in Zahlen um und gibt ihre Summe zurück. Benutze ? statt match. Gib das Ergebnis für ("10", "20") und für ("10", "zwanzig") aus. Tipp: Du brauchst ein ? pro parse, und die letzte Zeile packt die Summe in Ok(...).

Lösung anzeigen
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "zwanzig"));
}

Ausgabe:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ Selbst ausprobieren
  • Result<T, E> steht für eine Operation, die gelingen (Ok) oder mit einem Fehlerwert fehlschlagen (Err) kann.
  • match behandelt beide Fälle ausdrücklich – nach demselben Muster wie bei Option.
  • Der ?-Operator gibt ein Err automatisch vorzeitig zurück. So brauchst du nicht nach jedem Aufruf, der fehlschlagen kann, ein match.
  • Nimm lieber Result als panic! für Fehler, von denen sich ein Aufrufer sinnvoll erholen kann.

Kurzer Check

1. Welche zwei Varianten hat Result<T, E>?

2. Was macht der ?-Operator in einer Funktion, die ein Result zurückgibt?

3. Wann solltest du laut dieser Seite Result statt panic! nehmen?

4. Was gibt divide(10.0, 0.0) im Beispiel auf dieser Seite zurück?

5. Warum funktioniert der ?-Operator nur in einer Funktion, die selbst ein Result (oder eine Option) zurückgibt?

Punkte: 0 / 5