↑Programmieren in Rust
Wir wollen eine Funktion programmieren die ein Array nach einem
Wert durchsucht und den ersten Index zurückgibt wo dieser Wert gefunden
wurde. Es kann natürlich aber auch sein, dass dieser Wert nicht im Array
vorkommt. Man könnte in diesem Fall z. B. den negativen
Index -1
zurückgeben. Jedoch geben Enumerationen unsere
Absicht besser wieder.
Für solche optionalen Werte gibt es schon einen vordefinierten Datentyp:
enum Option<T> { Some(T), None }
Hierbei ist T
eine Typvariable für die ein
beliebiger aber fester Datentyp eingesetzt wird. In der Variante
None
trägt der Wert keine Daten. In der Variante
Some(T)
trägt der Wert genau einen Wert vom Typ
T
. Das Suchprogramm lässt sich damit so formulieren:
fn find(a: &[&str], s: &str) -> Option<usize> { for index in 0..a.len() { if a[index] == s {return Some(index);} } None } fn print_index(a: &[&str], s: &str) { match find(a,s) { Some(index) => println!("Der Index zu '{}' ist {}.", s, index), None => println!("Konnte '{}' nicht finden.", s) } } fn main() { let a = vec!["Kaffee", "Tee", "Mate"]; print_index(&a, "Tee"); print_index(&a, "Pfefferminztee"); }
Blieb der Erfolg bei der Ausführung einer Funktion aus, würde man gerne mehr darüber erfahren wie es dazu kam. Die Funktion muss dafür Information über den Fehler zurückgeben. Man könnte diese Information in einer zusätzlichen Variablen speichern, was jedoch mit dem Schönheitsfehler behaftet ist, dass diese Variable dann auch vernachlässigt werden könnte. Wir würden lieber korrektes Benutzen des Rückgabewertes erzwingen, was sich mittels Enumerationen bewerkstelligen lässt.
Für Resultate gibt es auch einen vordefinierten Datentyp:
enum Result<T, E> { Ok(T), Err(E) }
In der Variante Ok(T)
trägt der Wert einen normalen
Wert vom Typ T
, in der Variante Err(E)
einen Fehlerwert vom Typ E
.
Als Beispiel wollen wir ein Programm zur Umwandlung von
Zeichenketten in Zahlen schreiben. Kommen in der Zeichenkette aber
Zeichen vor die keine Ziffern sind, wird ein darüber Aufschluss
gebender Fehlerwert zurückgegeben. Einen entsprechenden Fehlerwert
soll es geben falls die Zahl größer ausfällt als u32
halten kann.
Zunächst die Funktion ohne Fehlerbehandlung:
fn int(s: &str) -> u32 { let mut value: u32 = 0; for c in s.chars() { let digit = u32::from(c) - u32::from('0'); value = 10*value + digit; } value }
Und nun mit Fehlerbehandlung:
enum ParseError {InvalidDigit, Overflow} fn int(s: &str) -> Result<u32, ParseError> { let mut value: u64 = 0; for c in s.chars() { if !c.is_ascii_digit() { return Err(ParseError::InvalidDigit); } let digit = u32::from(c) - u32::from('0'); value = 10*value + u64::from(digit); if value>u64::from(std::u32::MAX) { return Err(ParseError::Overflow); } } Ok(value as u32) } fn print_number(s: &str) { match int(s) { Ok(value) => println!("Zahl: {}", value), Err(ParseError::InvalidDigit) => println!("Keine Zahl."), Err(ParseError::Overflow) => println!("Zahl zu groß.") } } fn main() { print_number("12"); print_number("Firlefanz"); print_number("10000000000") }
Bei Verzicht auf detailierte Fehlerinformation erlaubt sich auch diese Ausgestaltung:
fn int(s: &str) -> Option<u32> { let mut value: u32 = 0; for c in s.chars() { if !c.is_ascii_digit() {return None;} let digit = u32::from(c) - u32::from('0'); value = value.checked_mul(10)?.checked_add(digit)?; } Some(value) }
Die Operationen checked_mul
und checked_add
überprüfen, ob es bei der Multiplikation zu einem Überlauf kommt, der
Rückgabewert hat den Typ Option<u32>
. Die
Operation x?
ist eine Kurzschreibweise für:
match x { Some(value) => value, None => return None }
Das ist ein sogenannter Wächter-Ausdruck. Falls x
den Wert None
haben sollte, wird die Ausführung
der Funktion abgebrochen und None
zurückgeben. Da
im weiteren Verlauf also None
ausgeschlossen ist,
kann value
einfach aus Some(value)
herausgeholt werden.
Den Fragezeichen-Operator gibt es auch für Resultate:
match x { Ok(value) => value, error => return error }
Die vollständige Definition des Operators kann zudem eine Konvertierung zwischen Fehlerarten vornehmen:
match x { Ok(value) => value, Err(error) => return Err(From::from(error)) }
Nicht immer ist die lokale Verarbeitung von Fehlern zielführend. Wie schon dargelegt, besteht die Vorgehensweise dann im Weiterreichen des Fehlers nach oben an den Aufrufer. Dabei tut sich nun recht schnell die Unbequemlichkeit auf, dass eine Funktion zwei oder mehr unterschiedliche Fehlertypen zurückgeben können muss.
Natürlich ließen sich die Fehlertypen mittels Enumeration zu einem gemeinsamen Typ zusammenfassen. Bei fortwährendem Weiterreichen kann dies allerdings zum umständlichen Verschachteln oder Neugruppieren von Enumerationen führen.
Was wir bräuchten wäre so eine Art Enumeration aus unendlich
vielen Typen. Solche Datentypen gibt es in Rust tatsächlich, sie
heißen Trait-Objekt-Typen und werden mit dem Schlüsselwort
dyn
eingeleitet. Hinter dem Schlüsselwort befindet
sich der Name einer Traitsignatur, in welcher die abstrakte
Schnittstelle des Typen kodiert ist. Konzeptuell kommt weiter hinzu,
dass dyn
eine Zusammenfassung von Typen
aller möglichen Speichergrößen ist. Aus diesem Grund kann
dyn Trait
keine feste Speichergröße besitzen – man
spricht von einem Typ dynamischer Größe. Um überhaupt
damit arbeiten zu können, darf ein solcher Typ nur eingehüllt durch
einen Zeiger vorkommen – bspw. als Referenz
&dyn Trait
oder mittels Smart-Pointer-Typ
Box
als Box<dyn Trait>
.
Die Standardbibliothek enthält so einen Trait
std::error::Error
. Damit lässt sich ein Kontrollfluss
formulieren, welcher dem aus anderen Programmiersprachen
bekannten Werfen und Abfangen von Ausnahmen nahe kommt.
type Error = Box<dyn std::error::Error>; fn get_path() -> Result<String, String> { match std::env::args().nth(1) { Some(path) => Ok(path), None => Err(String::from( "Kommandozeilenargument erwartet")) } } fn load() -> Result<String, Error> { let path = get_path()?; let text = std::fs::read_to_string(path)?; Ok(text) } fn main() -> Result<(), Error> { let text = load()?; println!("{}", text); Ok(()) }
Das Programm verhält sich so als würde im Fehlerfall
panic
geworfen. Was haben wir demgegenüber nun gewonnen?
Nun, Fehler lassen sich jetzt an jeder Stelle nach Wunsch abfangen,
umformen und verarbeiten.
Zum Schluss noch eine Warnung. Ein Wert vom universellen Fehlertyp
Box<dyn std::error::Error>
ist –
gerade weil er alle möglichen Fehler enthalten kann –
untypisiert. Man sollte Programme immer so stark wie
möglich typisieren und solche universellen Typen nur als letzten
Ausweg benutzen. Andernfalls kann es leichter dazu kommen, dass
ein bestimmtes Programmverhalten übersehen bzw. falsch verarbeitet
wird.
Beim Ablauf eines Programms kann es zu unkritischen Fehlern kommen, die zwar bedeuten, dass eine bestimmte Funktionalität nicht richtig arbeitet, allerdings nicht sofort zum Programmabbruch führen müssen. Nur weil beim einem Flugzeug beispielsweise ein Instrument oder Navigationsgerät ausfällt, muss das nicht gleich heißen, dass die gesamte Avionik den Geist aufgibt. Damit solche Fehler nicht übersehen werden, sollten diese zumindest in einen Fehler-Log einfließen. Das kann man z. B. so bewerkstelligen:
fn log_err(file: &str, line: u32, text: String) { eprintln!("Huch, da ist ein Fehler in Datei {}, \ Zeile {} aufgetreten:\n{}", file, line, text); } fn main() { const DEFAULT: u8 = 0; let x: u32 = 1000; let y = u8::try_from(x).unwrap_or_else(|err| { log_err(file!(), line!(), err.to_string()); DEFAULT }); println!("{}", y); }
In bestimmten Situation kann die Definition neuer individueller
Fehlertypen pragmatisch sein. Soll ein individueller Fehlertyp in den
universellen Fehlertyp umwandelbar sein, muss eine Implementierung
des Traits std::error::Error
vorliegen. Das bedeutet
an sich lediglich, dass die Traits Display
und
Debug
implementiert sind. Man bewerkstelligt das so:
use std::fmt::{self, Display}; type Error = Box<dyn std::error::Error>; #[derive(Debug)] struct CustomError {text: String} impl CustomError { fn new(text: &str) -> Self {Self {text: text.to_string()}} } impl Display for CustomError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.text) } } impl std::error::Error for CustomError {} fn main() -> Result<(), Error> { Err(CustomError::new("Fehler").into()) }