Fout, heel fout en paniek
Arnout van Kempen over rommelen in een digitale wereld.
Een aardige eigenschap van Rust is dat de compiler je simpelweg niet toestaat heel veel voorkomende fouten te maken. Fouten die in C gewoon gecompileerd zouden worden, om pas tijdens uitvoering van je programma tot problemen te leiden. Het systeem van ownership, mutability en borrowing dat we zagen, draagt daar sterk aan bij.
Het feit dat je bij een match alle mogelijke opties moet behandelen, omdat de compiler anders niet meewerkt, is er ook zo een. En dan geeft de compiler je nog een reeks waarschuwingen, die weliswaar wel mogen, maar niet verstandig zijn.
Maar dan nog kan een programma fouten maken, die de compiler niet kon voorzien. Je wilt een bestand openen, maar dat bestaat niet. Invoer van de gebruiker past niet, je wilt een element van een array benaderen dat niet bestaat, je wil delen door nul en ga zo maar door. Allemaal zaken die de compiler niet kan zien, maar die wel degelijk voor een probleem zorgen.
Anders dan in C, waar het netjes is foutafwikkeling in je programma op te nemen, is het in Rust vaak gewoon verplicht en krijg je tools om het altijd vrij makkelijk te maken. En als er dan iets mis gaat, krijg je nog duidelijke foutmeldingen ook. Dus laten we eens kijken hoe we met fouten kunnen omgaan.
Fouten variëren. Soms is een fout onoplosbaar. Dan wil je gewoon dat het programma stopt, wellicht een foutmelding geeft, maar verder niets. Bijvoorbeeld als je argumenten had moeten meegeven aan het programma en dat heb je niet gedaan. Dan moet de gebruiker uitleg krijgen over hoe het programma te gebruiken, maar dan moet het wel stoppen. Andere fouten zijn oplosbaar. Bijvoorbeeld als je een bestand wilt openen en dat bestaat niet. Dan kan je het oplossen door dat bestand alsnog aan te maken.
Rust heeft hier een reeks aan mogelijkheden en we beginnen met de meest onoplosbare problemen. Stel dat zich een situatie voordoet waarbij je wil dat je programma onmiddellijk stopt. Niet eens meer geheugen vrijgeven, geopende bestanden sluiten, niets meer, het moet gewoon direct voorbij zijn. Dan gebruik je in Rust:
std::process::abort
Na dit commando is je programma direct klaar. Het besturingssysteem moet eventueel maar geheugen terughalen, bestanden sluiten, of niet, maar je programma doet niks meer.
Een iets minder acuut einde is een panic. Als je die macro aanroept, kan je nog een foutmelding meegegeven. Je programma geeft nu alle gereserveerd geheugen terug, de stack wordt netjes leeggemaakt en geopende bestanden worden gesloten, en de buffers worden geflushed. Daarna stopt je programma. Dat gaat met:
panic!(“foutmelding”);
Maar echt fraai wordt het als je zelf functies schrijft, of je maakt gebruik van Rust-functies, waarbij je weet dat het fout zou kunnen gaan. Het openen van een bestand zou fout kunnen gaan. Twee variabelen op elkaar delen ook. Wat je dan doet, is de uitkomst van die functie in een enum stoppen, die standaard in Rust zit. Net als de Option<T> die we eerder zagen die NULL-pointers voorkomt. Nu gebruiken we de enum Result<T, E>. T is het type van de functie-uitkomst en E is een string met een errormelding. Neem de functie:
fn delen(teller: f64, noemer: f64) -> Result<f64, String> {
if noemer == 0.0 {
return Err(“Noemer mag niet nul zijn”.to_string());
}
Ok(teller / noemer)
}
Je wilt eigenlijk als uitkomst gewoon teller/noemer hebben. Maar door die uitkomst te verpakken in een Result-enum kan je de mogelijke fout die zou ontstaan als je door nul zou delen voorkomen en op een eigen manier opvangen. Het probleem is nu alleen dat de uitkomst van deze functie niet de gewenste deling is. Die zit verpakt in Ok().
Hoe haal je die er dan uit? Nou, zoals dat gaat met een enum, via pattern-matching:
let resultaat = delen(5.0, 3.0);
match resultaat {
Ok(waarde) => println!(“Resultaat: {}”, waarde),
Err(error) => println!(“Foutmelding: {}”, error),
}
zou werken. Er zijn compactere en geavanceerdere manieren, maar dit is al een keurige manier om de waarde uit het Result te vissen en meteen eventuele fouten goed te adresseren, zonder dat ze zich daadwerkelijk voordoen.
Als je vrij slordig en haastig programmeert, omdat je 100 procent zeker weet dat je functie toch geen error zal opleveren, dan kan het met de methode unwrap(). Die haalt zonder enige check de waarde uit Ok(), maar als zich toch een fout voordoet, eindigt dat in een panic.
De code is wel lekker kort en simpel:
let resultaat = delen(5.0, 3.0);
let waarde = resultaat.unwrap();
Als je vrij zeker weet dat een error is uitgesloten, maar je bent niet helemaal zeker, dan gebruik je de methode expect(), waarbij je een tekst mee geeft voordat je programma in een panic belandt:
let resultaat = delen(5.0, 3.0);
let waarde = resultaat.expect(“Kan de waarde niet berekenen”);
Op GitHub staat code waar al deze opties worden gedemonstreerd. Experimenteer daar vooral eens mee. Haal dan steeds een blokje uit de commentaar-stand en zet wat je niet gebruikt weer buiten werking door // er voor te plaatsen. Let op wat je programma doet en kijk vooral ook goed naar de waarschuwingen en de panics van compiler en runcode.
Een ding dat niet op GitHub staat, maar wat ik wel wil noemen: als je een functie hebt die een Result<T, E> retourneert en je hebt een andere functie die die eerste functie aanroept, die een Result van datzelfde type retourneert, dan kan je in die aanroepende functie simpelweg een vraagteken gebruiken achter de functie-aanroep. Als de uitkomst dan een Err() is, dan eindigt de functie direct en geeft de Err() die ontvangen werd direct door als return-waarde van de eigen functie. (Werkt trouwens ook met Option<T>)
Bij een ketting van elkaar aanroepende functies met een Result<T, E> hoef je dus niet iedere keer te matchen, maar een simpel vraagteken is genoeg.
Wie mee wil doen met #klooienmetcomputers kan dat doen via GitHub. Maak een account op github.com en zoek naar Abmvk/kmc. Het account Abmvk volgen kan ook. Lezers zijn vrij te gebruiken wat ze willen en om zelf zaken toe te voegen of aan te passen, vragen te stellen of commentaar te leveren.
Gerelateerd
Gewone variabelen
Arnout van Kempen over rommelen in een digitale wereld.
Bestanden in soorten en maten
Arnout van Kempen over rommelen in een digitale wereld.
Alles draait om data
Arnout van Kempen over rommelen in een digitale wereld.
Omgeving: input-output
Arnout van Kempen over rommelen in een digitale wereld.
Omgeving: de configuratie
Arnout van Kempen over rommelen in een digitale wereld.