Programmierkonzepte und Ideen bei Rust

Rust ist eine Programmiersprache, die sich in vielem von vorherigen Sprachen unterscheidet. Grundsätzlich ist dies immer nachteilig, da der Lernaufwand dadurch wächst. Aber Rust bringt auch andere Konzepte mit, weshalb es richtig ist, auch neue Begriffe zu nutzen, um gerade Verwechslungen zu vermeiden.

Pakete sind Kisten

Einen Begriff, den man vielleicht auch von anderen Systemen hätte übernehmen können, sind Pakete, aber in Rust werden Pakete als crates (engl. Kisten) bezeichnet. Zur Verwaltung dieser Kisten wird das Programm cargo (engl. Fracht) verwendet, mit dem Kisten angelegt, übersetzt, ausgeführt, vom Server geladen oder veröffentlicht werden können.

Das Programm cargo ist stark mit Rust verschnürt und lässt sich ähnlich wie git über einen Aktionsparameter steuern; z. B. cargo run zum Ausführen oder cargo clean zum Aufräumen.

Die Informationen zu einer Kiste sind im Hauptverzeichnis in der Textdatei Cargo.toml hinterlegt. Darin sind der Name der Kiste und des Autors, die Version und Abhängigkeiten zu anderen Kisten hinterlegt. Eine Kiste kann auch ein Workspace sein und weitere eigenständige Kisten enthalten, was vor allem in Kombination mit git praktisch ist, da man so ein Projekt mit allen Teilen (z. B. Bibliotheken) zusammenhalten kann.

Eine Liste interessanter Kisten

struct + trait + impl

Typerkennung

Quelltext aller Abhängigkeiten liegt vor

zwingt praktisch zu Open-Source

Gute Bezeichner

u8 für eine vorzeichenlose Ganzzahl (engl.: unsigned) mit 8 Bit oder i32 für eine Ganzzahl mit Vorzeichen (eng.: integer) mit 32 Bit.

fn zur Definition eine Funktion. pub

Gute Vorgaben

Beim Design der Sprachen haben die Rust-Entwickler meiner Beobachtung nach einen guten Blick auf die Wahl von Standardwerten und -verhaltensweisen der Elemente der Sprache gelegt und somit schon Problemen entgegengewirkt, die durch den Schreibstil entstehen. Rust erleichtert daher in vielen Punkten das Schreiben von gutem Quellcode.

Variablen sind konstant

Für den Compiler ist es hilfreich zu wissen, ob sich er Wert einer Variablen ändern kann bzw. ob ein Objekt veränderbar ist. Vor allem aus dem Wissen, dass ein Objekt unveränderlich ist, lassen sich Optimierungen des Binärcodes herleiten. Auch für die Fehlerrate des Quellcodes ist diese Kennzeichnung der Variablen sinnvoll, denn damit kann der Compiler auch auf ungewollte Änderungen prüfen. Meiner Beobachtung nach wird einem Großteil der Variablen nur einmal ein Wert zugewiesen und häufig Objekte nur zum Lesen verwendet.

Viele andere Programmiersprachen habe auch eine Möglichkeit, Variablen oder Objekte als konstant zu markieren, jedoch sind Programmierer meist faul und unterlassen diese Kennzeichnung. Daher finde ich es bei Rust sehr gut, dass das Prinzip umgekehrt wurde und die einfache Variablendeklaration let a : u32 = 12 zu einer konstanten Variablen führt. Erst mit dem Zusatz mut kann man eine Variable nach der ersten Zuweise nochmals verändern.

Überlaufprüfungen bei Rechenoperationen

% rustc -o test - <<__EOF && ./test
fn main() {
    let a: u8 = u8::max_value();

    println!("b={:?}", a + 1);
}
__EOF
thread 'main' panicked at 'attempt to add with overflow', :4:24
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Prüfungen mit speziellen Methoden checked_add:

% rustc -o test - <<__EOF && ./test
fn main() {
    let a: u8 = u8::max_value();
    match a.checked_add(1) {
        Some(r) => println!("r={}", r),
        None => println!("Failed to calculate {} + 1", a),
    }
}
__EOF
Failed to calculate 255 + 1

Rust ist explizit

Rust hat keine Exceptions

Fehler werden in Rust nicht nach dem Exception-Prinzip einer zweiten Programmablaufstruktur, sondern im normalen Kontrollfluss übermittelt. Rust gleicht in dem Punkt mehr der Fehlerbehandlung von C, wo der Fehler ein regulärer Rückgabewert einer Funktion ist, wobei sich das in Rust eleganter durch komplexe Typen lösen lässt, wo in C nur primitive Typen möglich sind.

Während man in C zum Beispiel bei einer Längenangabe die Werte eines ints verstümmelt und sagt »alle gültigen Werte sind positiv, alle negativen Werte sind Fehlercodes«, ist in Rust der Rückgabewert vom Typ Result, der sich aus den zwei Teilen Ok für den Erfolgsfall und Err für den Fehlerfall zusammensetzt.

Die Mischung von Fehler und Erfolg wie in C hat man nicht und kann somit den Vorteil nutzen, dass man nicht wie bei Exceptions einen zweiten Kontrollfluss aufbaut, sondern den regulären Kontrollfluss nutzt. Hierzu muss man überlegen, wie Exceptions in Programmiersprachen umgesetzt werden: Beim Erreichen eines try-Blocks registriert der Compiler die Stelle des catch- oder finally-Blocks in einer Liste und wenn eine Ausnahme ausgelöst wird, wird der normale Kontrollfluss, wie er über den Stack aufgebaut wurde, verlassen und gemäß der Liste ein zweiter Pfad gegangen, bis man wieder in den alten Pfad auf dem Stack einsteigt.

Exceptions sind gut, aber mit Rusts Ansatz ist die Komplexität geringer. Der Fehlerfall ist in Rust allgegenwärtig und man wird gezwungen, darüber nachzudenken, denn das ist bei Exceptions leider viel zu häufig das Problem, dass diese nur ordnungsgemäß beachtet werden, weil sie eben am regulären Programmablauf leicht vorbeirauschen können.

Der Konstruktor

In Rust gibt es keine ausgezeichnete Methode, die nach dem Anlegen des Speichers eines Objekts zur Initialisierung aufgerufen wird. Anstelle des festen Konstruktors wie in anderen Programmiersprachen, definiert man eine statische Methode für den Typ, die den Speicher anfordert und dann initialisiert. Neu ist dieses Konzept nicht, denn in Perl kennt man es bereits.

Der Vorteil ist, dass man keinen zusätzlichen new-Operator benötigt, sondern die normale Syntax zum Aufruf von statischen Methoden verwendet: Type::new. Der Methodenname new wird häufig aus Konvention verwendet, aber in manchem Kontext mag auch der Name load zum Beispiel treffender sein, den man aufgrund der Freiheit wählen kann, so dass man Record::load(id) anstatt new Record(id) sagt, weil man den Datensatz lädt.

Weiterhin lässt sich so auch besser der Aufruf anderer Initialisierungsmethoden steuern, was bei anderen Programmiersprachen oft ein Krampf ist, wenn man Prüfungen oder andere Schritt vornehmen will, bevor man an einen zentralen Konstruktor abgibt.

Der Konstruktor kann somit zum Beispiel auch als Rückgabewert einen Fehler liefern, was bei Programmiersprachen mit festem Konstruktor nicht möglich ist und nur mit einer Exception gelöst werden kann. Da es in Rust keine Exceptions gibt, ist dieses Konzept der statischen Methode als Konstruktor gar zwingend.

Referenzen – &str und String

ref für match und »Move out of borrowed content«

& vs. ref in Rust patterns

Wie sind Closures implementiert?

Finding Closure in Rust

Typerkennung und Methodenauswahl

Rust kann anhand der Verwendung einer Variablen, deren Typ erkennen:

let uri = "http://example.org/".parse()?;
let work = web_client.get(uri);

Referenzen, Bücher, Einführungen