Rust-Tutorial, Teil 1: Sprachkonstrukte, Ownership und asynchrone Programmierung
Zirkeltraining
Rust arbeitet ohne Laufzeitumgebung und Garbage Collector. Mit dem Ownership-Konzept gewährt die Sprache Speichersicherheit, ohne Performance einzubüßen.
Das 2010 entstandene Rust-Projekt hat es sich zum Ziel gesetzt, eine praktikable, sichere und nebenläufige Systemsprache zu entwickeln, mit der Anwendungen dieselbe Laufzeitgeschwindigkeit und Speichereffizienz erhalten wie in C erstellte Programme. Dieses zweiteilige Tutorial gibt eine Einführung in die wichtigsten Konstrukte der Sprache, erläutert das Ownership-Konzept und beschäftigt sich mit der Speicherverwaltung. Der erste Teil stellt Tupel, Enums und Traits vor und zeigt am Beispiel eines Webservers die Vorteile der asynchronen Programmierung.
Die Rust-Syntax ähnelt der von C und C++ insofern, als Entwicklerinnen und Entwickler aufeinanderfolgende Anweisungen mit Semikolon voneinander trennen und Blöcke über geschweifte Klammern markieren:
Hello World in Rust fn main() { println!("Hello, world!"); }
Allerdings weicht die Syntax auch von der von C ab, wie die Variablendeklaration zeigt:
let x: Typ = Wert; let mut v: Typ = Initialwert; v = NeuerWert;
Zudem gibt es einen semantischen Unterschied: Variablen sind in der Regel unveränderlich, es sei denn, man markiert sie über das Schlüsselwort mut
als mutable
. Den Versuch, der Variablen x
einen neuen Wert zuzuweisen, erkennt der Compiler und gibt eine Fehlermeldung aus. Die Angabe des Typs ist dabei optional, zumindest solange er sich über die Zuweisung bestimmen lässt. So erzeugt let x = 42
eine 32-Bit-Ganzzahlvariable mit Vorzeichen (Rust nennt diesen Typ i32), da Ganzzahlwerte ohne weiteres Suffix als solche interpretiert werden.
Einen Check vornehmen
Ganzzahlwerte können über ein Suffix andere Typen angeben: 42u64
ist die Zahl 42 als 64-Bit-Wert ohne Vorzeichen und entsprechend lässt sich mit let x = 42u64
oder let x: u64 = 42
eine 64-Bit-Variable ohne Vorzeichen anlegen. Weitere Typen zeigt die Tabelle. Ungewöhnlich sind dort isize
und usize
, die immer dieselbe Größe wie der maximal adressierbare Speicherbereich haben, also 64 Bit bei einem für ein 64-Bit-OS kompilierten Programm. Entsprechend sollten Anwender usize
für alles benutzen, was direkt oder indirekt mit der Adressierung von Speicher zu tun hat, wie etwa für die Indizierung in Containern. Eine Besonderheit von Rust ist, dass im Debug-Modus kompilierter Code einen Bereichscheck für alle Ganzzahloperationen durchführt und im Falle eines Überlaufs das Programm mit einem Fehler beendet.
Ganzzahl- und Fließkommatypen | |||
Größe | Ganzzahl mit Vorzeichen | Ganzzahl ohne Vorzeichen | Fließkommazahl |
8 Bit | i8 | u8 | – |
16 Bit | i16 | u16 | – |
32 Bit | i32 | u32 | f32 |
64 Bit | i64 | u64 | f64 |
128 Bit | i128 | u128 | – |
OS- und CPU-Architektur-abhängig | isize | usize | – |
Die fett hervorgehobenen Typen sind der Standardtyp für Werte ohne Suffix. |
Rust bietet verschiedene Möglichkeiten an, zusammengesetzte Werte in einer Variablen abzuspeichern, am einfachsten per Tupel. Der folgende Codeabschnitt zeigt, wie man ein Tupel anlegt und auf Werte zugreifen kann:
let tup = (500, 6.4, 1); let (x, y, z) = tup; assert!(x == tup.0);
Auch Arrays kennt die Sprache. Der folgende Code legt zwei identische Arrays a
und b
an, für das Array b
gibt man nur den Typ und seine Größe mit [i32; 5]
an:
let a = [1, 2, 3, 4, 5]; let b: [i32; 5] = [1, 2, 3, 4, 5]; let index = 9; let element = a[index];
Die Variable element
versucht, auf das zehnte Element von Array a
zuzugreifen, das allerdings nur die Größe 5 besitzt. In vielen Sprachen würde dies zu einem illegalen Speicherzugriff führen. Rust führt aber eben für alle Arrayzugriffe einen Bereichscheck durch. Kompiliert man das Beispiel mit einer alten Version des Rust-Compilers, gibt er zur Laufzeit noch einen Fehler aus. Neue Versionen des Compilers sind hingegen in der Lage zu erkennen, dass immer ein Laufzeitfehler ausgelöst würde, und weigern sich, den Code zu kompilieren.