Panic in Rust
Eine Panic ist Rusts Art, ein Programm sofort anzuhalten, wenn etwas so schiefgelaufen ist, dass es sich nicht mehr retten lässt. Wenn du die bisherigen Seiten mitgemacht hast, hast du wahrscheinlich schon ein paar ausgelöst, ohne es zu wollen: Ein Index hinter dem Ende eines Arrays oder .unwrap() auf einem None lösen beide eine Panic aus.
Eine Panic selbst auslösen
Abschnitt betitelt „Eine Panic selbst auslösen“fn main() { panic!("etwas ist gründlich schiefgelaufen");}thread 'main' (12345) panicked at src/main.rs:2:5:etwas ist gründlich schiefgelaufennote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceDie Zahl in Klammern, hier (12345), ist die ID des Threads. Sie ist bei jedem Lauf anders, und ältere Rust-Versionen zeigen sie gar nicht an.
Wenn das läuft, gibt das Programm diese Meldung aus und hält an. Nach dem panic! wird nichts mehr ausgeführt. Die erste und die letzte Zeile stammen von Rust selbst und sind deshalb auf Englisch: Der Thread main ist an der genannten Stelle in Panik geraten, und mit der Umgebungsvariable RUST_BACKTRACE=1 bekommst du einen Backtrace – also die Liste der Funktionsaufrufe, die zur Panic geführt haben.
Was eine Panic auslöst, ohne dass du panic! aufrufst
Abschnitt betitelt „Was eine Panic auslöst, ohne dass du panic! aufrufst“Ein paar Operationen, die du schon kennst, lösen von selbst eine Panic aus, wenn etwas nicht stimmt. Jedes Beispiel unten bekommt den riskanten Wert über einen Funktionsparameter statt als feste Zahl im Code. Der Grund: Steht der Index oder die 0 direkt im Code, erkennt der Compiler oft schon beim Kompilieren, dass die Operation immer scheitert. Dann baut er das Programm gar nicht erst, statt es laufen und in Panik geraten zu lassen.
fn get(numbers: &[i32], index: usize) -> i32 { numbers[index] // Panic: Index außerhalb des gültigen Bereichs}
fn main() { let numbers = [1, 2, 3]; println!("{}", get(&numbers, 10));}fn main() { let value: Option<i32> = None; println!("{}", value.unwrap()); // Panic: called `Option::unwrap()` on a `None` value}fn divide(a: i32, b: i32) -> i32 { a / b // Panic: attempt to divide by zero}
fn main() { let a = 10; let b = 0; println!("{}", divide(a, b));}Die Meldungen in den Kommentaren zeigen, was Rust auf Englisch ausgibt: unwrap() wurde auf einem None-Wert aufgerufen bzw. es wurde versucht, durch null zu teilen. In jedem dieser Fälle weigert sich Rust, dein Programm in einem Zustand weiterlaufen zu lassen, der keinen Sinn ergibt – statt still irgendeinen Unsinn zurückzugeben.
panic! oder Result?
Abschnitt betitelt „panic! oder Result?“Diese Entscheidung triffst du oft: Soll dieser Fehlschlag das Programm abbrechen, oder soll der Aufrufer die Chance bekommen, ihn zu behandeln?
panic!– für Bugs und Situationen, die bei korrektem Code nie vorkommen dürften. Es gibt keinen sinnvollen Weg, sich davon zu erholen. Sofort anzuhalten (bevor noch mehr Schaden entsteht), ist dann die sicherste Wahl.Result(siehe vorherige Seite) – für Fehlschläge, mit denen ein Aufrufer rechnen kann und die er behandeln möchte: falsche Benutzereingaben, eine fehlende Datei oder eine gescheiterte Netzwerkanfrage.
// Falsche Eingabe vom Benutzer – davon kann man sich erholen, also Resultfn parse_age(input: &str) -> Result<u32, String> { input.parse().map_err(|_| String::from("kein gültiges Alter"))}
// Eine Bedingung, die bei korrektem Code nie verletzt sein kann – also panicfn get_first(numbers: &[i32]) -> i32 { if numbers.is_empty() { panic!("get_first wurde mit einem leeren Slice aufgerufen"); } numbers[0]}unwrap() und expect() sind Abkürzungen zur Panic
Abschnitt betitelt „unwrap() und expect() sind Abkürzungen zur Panic“Du hast .unwrap() schon bei Option und bei Result gesehen. Eigentlich ist es nur eine Abkürzung für „Gib mir den Wert – oder lös eine Panic aus, wenn keiner da ist.“ .expect("Meldung") macht dasselbe, lässt dich aber eine hilfreichere Meldung mitgeben:
fn main() { let input = "42"; let number: i32 = input.parse().expect("die Eingabe sollte eine gültige Zahl sein"); println!("{number}");}Schlägt die Umwandlung fehl, enthält die Panic-Meldung deinen Text. Das macht die Fehlersuche viel leichter als die allgemeine Meldung, die .unwrap() liefert.
Annahmen prüfen: assert!, assert_eq! und unreachable!
Abschnitt betitelt „Annahmen prüfen: assert!, assert_eq! und unreachable!“Manchmal weißt du, dass an einer bestimmten Stelle im Code etwas wahr sein muss. Rust hat ein paar kleine Makros, die aus dieser Überzeugung eine Prüfung machen. Schlägt die Prüfung fehl, lösen sie eine Panic mit einer klaren Meldung aus. So zeigt sich ein Bug genau dort, wo er entsteht, statt später für seltsame Ergebnisse zu sorgen.
assert!(condition, "message")löst eine Panic aus, wenn die Bedingungfalseist.assert_eq!(left, right)löst eine Panic aus, wenn die beiden Werte nicht gleich sind, und gibt beide aus.unreachable!("message")markiert Code, der nie laufen sollte, zum Beispiel einenmatch-Zweig für einen Wert, den es nicht geben kann.
fn average(numbers: &[i32]) -> i32 { assert!(!numbers.is_empty(), "average braucht mindestens eine Zahl");
let mut total = 0; for n in numbers { total += n; } total / numbers.len() as i32}
fn day_name(day: u8) -> &'static str { match day { 1 => "Montag", 2 => "Dienstag", 3 => "Mittwoch", 4 => "Donnerstag", 5 => "Freitag", 6 => "Samstag", 7 => "Sonntag", _ => unreachable!("day muss zwischen 1 und 7 liegen, war aber {day}"), }}
fn main() { let avg = average(&[70, 80, 90]); assert_eq!(avg, 80); println!("Durchschnitt: {avg}");
println!("Tag 3 ist {}", day_name(3));}Ausgabe:
Durchschnitt: 80Tag 3 ist MittwochHier bestehen alle Prüfungen, also läuft das Programm ganz normal. Ändere jetzt im Playground die Liste zu [70, 80, 95]. Der Durchschnitt wird 81, und assert_eq! hält das Programm an und zeigt beide Werte nebeneinander:
thread 'main' (12345) panicked at src/main.rs:26:5:assertion `left == right` failed left: 81 right: 80Diese Meldung kommt von Rust selbst und ist deshalb auf Englisch: Die Prüfung „left == right“ ist fehlgeschlagen, links steht der berechnete Wert, rechts der erwartete. Probier auch average(&[]) oder day_name(9) aus, dann siehst du deine eigenen Meldungen in der Panic.
Diese Prüfungen laufen immer, auch in einem Release-Build. Halte sie also billig. assert_eq! begegnet dir auf der Seite Testen wieder: Dort ist es der wichtigste Weg, mit dem ein Test sagt „das hier sollte gleich dem da sein“. Außerdem gibt es todo!(). Das löst eine Panic mit „not yet implemented“ (noch nicht implementiert) aus. Es ist praktisch als Platzhalter-Rumpf, während du eine Funktion skizzierst, die du später fertig schreibst.
Übungen
Abschnitt betitelt „Übungen“1. Eine Portnummer mit expect
Abschnitt betitelt „1. Eine Portnummer mit expect“Du hast einen Konfigurationswert let port_text = "8080";. Mach daraus mit .parse() und .expect(...) ein u16, mit einer Meldung, die erklärt, was schiefgelaufen ist. Gib dann Lausche auf Port 8080 aus. Tipp: Die Variable braucht einen Typ, let port: u16 = ..., damit parse weiß, was es erzeugen soll. Ändere den Text danach zu "80a0", um deine Meldung in der Panic zu sehen.
Lösung anzeigen
fn main() { let port_text = "8080"; let port: u16 = port_text .parse() .expect("der Port in der Konfiguration sollte eine Zahl von 0 bis 65535 sein"); println!("Lausche auf Port {port}");}Ausgabe:
Lausche auf Port 80802. Die Rechnung teilen
Abschnitt betitelt „2. Die Rechnung teilen“Schreib fn split_bill(total: u32, people: u32) -> u32, die den Anteil jeder Person zurückgibt. Eine Rechnung durch null Personen zu teilen, ist ein Bug im aufrufenden Code. Fang ihn deshalb mit assert! und einer klaren Meldung ab. Gib den Anteil für einen Betrag von 120 und 4 Personen aus. Tipp: Schreib das assert! in die erste Zeile der Funktion, vor die Division.
Lösung anzeigen
fn split_bill(total: u32, people: u32) -> u32 { assert!(people > 0, "split_bill braucht mindestens eine Person"); total / people}
fn main() { let share = split_bill(120, 4); println!("Jede Person zahlt {share}");}Ausgabe:
Jede Person zahlt 30Zusammenfassung
Abschnitt betitelt „Zusammenfassung“panic!hält das Programm sofort an und gibt eine Fehlermeldung aus.- Manche Standardoperationen (ein ungültiger Array-Index,
.unwrap()aufNone, Division durch null) lösen automatisch eine Panic aus. - Nimm
panic!für Bugs und Bedingungen, die nie verletzt sein dürften; nimmResultfür Fehlschläge, die ein Aufrufer sinnvoll behandeln kann. .expect("Meldung")ist wie.unwrap(), aber mit einer Meldung, die die Fehlersuche bei einer Panic erleichtert.assert!,assert_eq!undunreachable!lösen absichtlich eine Panic aus, wenn sich etwas, das du für wahr gehalten hast, als falsch herausstellt.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was passiert, wenn du panic! aufrufst?
2. Wann solltest du laut der Faustregel auf dieser Seite panic! statt Result nehmen?
3. Was ist der Unterschied zwischen .unwrap() und .expect('Meldung')?
Punkte: 0 / 3
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Nicht behebbare Fehler mit panic! – Das Rust-Buch: was bei einer Panic passiert, was „Unwinding“ (Abwickeln des Stacks) bedeutet und wie du einen Backtrace liest.
- Wann panic! aufrufen und wann nicht? – Das Rust-Buch: die offiziellen Richtlinien für die Wahl zwischen
panic!undResult, mit Beispielen. - panic! – Rust by Example (auf Englisch): ein kurzes Beispiel für eine Panic, das zeigt, dass der Speicher beim Abwickeln trotzdem freigegeben wird.
assert_eq!– Doku der Standardbibliothek (auf Englisch): wie die Assertion-Makros funktionieren, auch mit eigenen Meldungen.panic!– Doku der Standardbibliothek (auf Englisch): die Referenz zupanic!selbst, wie es mitunwrapzusammenhängt und wann eine Panic das richtige Werkzeug ist.