In den Programmiersprachen C oder auch in Python, können Variablen gültige und «ungültige» Zustände annehmen, wie zum Beispiel NULL oder None. Auch in C# gibt gab es ein Problem mit NullReferenceExceptions. Rust kennt ein solches Konzept nicht. In Rust ist dies mit den Typen Option und Result gelöst. Zusätzlich übernimmt der Compiler von Rust Überprüfungen, die wir uns angewöhnt haben, selbst zu machen und darauf bedacht sind, diese nicht zu vergessen und auch bei einem Review zu kontrollieren. Dadurch werden Rust-Funktionen einfacher als die C-Funktionen.
Starten wir mit einem einfachen (gesuchten) Beispiel in C. Eine Funktion, die nicht in jedem Fall erfolgreich ist:
#include "stdbool.h"
#include "stdio.h"
bool sqrt_of_nine(int input, int *output) // some function that returns a result and a success bool
{
// check the input pointer for validity
if (!output) {
return false;
}
// general input validation
if (input < 0) {
// cannot calculate the square root of a negative number
return false;
}
// run the actual function that will succeed
*output = input / 3;
return true;
}
void main()
{
int a = 9;
int b = 0;
sqrt_of_nine(a, &b);
printf("%d", b);
}
Das C-Beispiel kann mit dem Compiler-Explorer getestet werden (Sprache C, Compiler x86-64 gcc 14.1).
Wir haben eine Funktion, die Parameter entgegennimmt und ein Resultat zurückgibt. Es kann jedoch sein, dass das Resultat nicht gültig ist, weil etwas nicht funktioniert hat. Klassisch löse ich das in C mit einem Bool als Return-Value und dem Resultat als Pointer. Dafür sind einige selbst geschriebene Checks und die dazugehörige Disziplin notwendig. Ups! Die Überprüfung wurde bereits im Beispiel vergessen. Keiner zwingt uns den Pointer zu überprüfen oder den zurückgegebenen Bool auch effektiv zu verwenden. Dazu kommt, dass diese Überprüfungen typischerweise über mehrere Ebenen hinweg mitgezogen werden.
In Rust sind 2 Dinge einfacher:
1. Wir können ein Resultat mit dem Erfolg vom Resultat verknüpfen
2. Strenge Typisierung: Es kann nicht passieren, dass wir aus Versehen mit einem ungültigen Wert weiterarbeiten, da der Typ der möglicherweise auch ungültig sein kann, ein anderer ist.
Das heisst wir können den gültigen Wert so verpacken, dass er eindeutig vom Ungültigen unterschieden werden kann und wir müssen den Input nur auf logische Gültigkeit überprüfen.
Dafür bietet sich Rusts Typ Option an. Ein Enum, der sogar Daten enthalten kann:
enum Option<T> {
Some(T),
None,
}
Es gibt also 2 Varianten, die möglich sind:
Some: Darin sind dann die gültigen DatenNone: Also ungültig und ohne DatenDas Rust Beispiel könnte dann wie folgt aussehen.
fn sqrt_of_nine(input: i32) -> Option<i32> {
// general input validation
if input < 0 {
// cannot calculate the square root of a negative number
return None;
}
// run the actual function that will succeed
return Some(input / 3);
}
fn main() {
let a = 9;
let res = sqrt_of_nine(a);
println!("{:?}", res)
}
Das Rust-Beispiel kann im Rust-Playground getestet werden (Rust Version 1.78).
Dieser Rückgabewert ist vom Typ Option und der Compiler kann (und wird!) die Verwendung überprüfen. Ich kann nicht aus Versehen mit dem ungültigen Wert weiterrechnen.
Alternativ gibt es auch den Typ Result. Ähnlich wie beim Typ Option gibt es 2 Varianten, jedoch statt None gibt es einen Error der ebenfalls Daten enthalten kann. Diese müssen nicht den gleichen Typ wie das gültige Resultat haben:
Ok: Enthält die gültigen Daten vom Typ <T>Err: Enthält die Daten des Fehlers vom Typ <E>enum Result<T, E> {
Ok(T),
Err(E),
}
Das Beispiel würde dann etwa so aussehen, das Resultat ist vom Typ i32 und für den Fehler verwenden wir ein Tuple, das einen Fehlercode i32 und einen String (um genau zu sein ein String-Slice) &str enthält, also den Typ (i32, &str):
fn sqrt_of_nine(input: i32) -> Result<i32, (i32, &'static str)> {
// general input validation
if input < 0 {
return Err((403, "Taking the negative square root is forbidden"));
}
// run the actual function that will succeed
return Ok(input / 3);
}
fn main() {
let a = 9;
let res = sqrt_of_nine(a);
println!("{:?}", res)
}
Das Rust-Beispiel kann im Rust-Playground getestet werden (Rust Version 1.78).
Auf Embedded Systemen interagieren wir viel über Schnittstellen mit anderen Teilnehmern. Dabei kann es vorkommen, dass etwas schief geht. Darum muss das Resultat der Funktion, welche über die Schnittstelle kommuniziert, ausgewertet werden, um im Fehlerfall etwas zu wiederholen oder abzubrechen.
Rust bietet für diese Art von Problemen zwei sehr gute Werkzeuge, um dem Compiler die Arbeit abzugeben. Der Compiler überprüft die Inputs und der Aufrufer hat keine Möglichkeit (oder muss sich extra Mühe geben) einen fehlerhaften Wert aus Versehen zu verwenden. Rust kann also auch C vereinfachen, nicht nur C++ (What makes Rust easier than C++)
Im Übrigen lohnt es sich sehr, das Rust Book – «The Book» zu lesen, um die Theorie hinter den Konzepten von Rust besser zu verstehen. Darin wird auch beschrieben wie denn die Typen Option und Result elegant entpackt und verarbeitet werden können.
Schreiben Sie einen Kommentar