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
- Als Funktionsparameter oder in Struct: String vs &str in Rust functions
- Funktionsparameter, die beide Varianten akzeptieren: Creating a Rust function that accepts String or &str
- Einsatz von
Cow
(Clone-on-write) als Rückgabewert: Creating a Rust function that returns a &str or String
ref für match und »Move out of borrowed content«
Lifetime
Bei <'a, 'b: 'a>
lebt 'b
genauso lang wie oder länger als 'a
. Der
Doppelpunkt ist also wie >=
bezogen auf die
Lebenszeit:
'b: 'a <=> lifetime(b) >= lifetime(a)
.
Implementierung von Async/Await
Wie sind Closures implementiert?
- Finding Closure in Rust: eine kürzere Erklärung
- Closures: Magic Functions: eine ausführliche Erklärung
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
- Übersicht an offiziellen Materialien
- API Reference
- The Rust programming language; deutsche Ausgabe
- Rust by example
- Cargo book
- A half-hour to learn Rust: sehr viele kleine Code-Schnippsel zur Erklärung von Rust
- Anleitung zum Erstellen eines WebAssembly (wasm) mit Rust
- Tour of Rust's Standard Library Traits
- Sammlung von Dokumentationen (macros, tokio, FFI, …)
Sonstiges
- mrustc: Alternativer Rust-Compiler
- cargo xtask: Interessante Idee, ein
Unterprojekt für spezielle Aufgaben zu erstellen und mit einer projektinternen
cargo-Konfiguration einen Alias für xtask anzulegen, um auf das Projekt zu
verweisen. Damit kann man
cargo xtask …
wiemake …
aufrufen und der Aufruf wird an das Unterprojekt weitergeben. - The Tectonic Typesetting System: TeX in Rust
- Rhai - An embedded scripting language for Rust
- Deno - A secure runtime for JavaScript and TypeScript
- quick-js — Rust web dev library // Lib.rs
- Rust bindings for Duktape, the embedded JavaScript engine
- Beispiele für die Anbindung an C
- Konverter für C nach Rust: C2Rust, Introduction, Handbuch
- Svgbob (Quelltext) zur Umwandlung von ASCII-Zeichnungen in SVG. Damit kann man sehr leicht Diagramme in Markdown zeichnen und dann die Grafik in die Datei einbetten. In Emacs kann man schön mit dem Artist-Mode zeichnen.
- awesome-alternatives-in-rust: Liste von Programmen und Bibliotheken, die in Rust nachprogrammiert wurden