Die Programmiersprache Rust

Willkommen! Dieses Buch wird dir die Programmiersprache Rust beibringen. Rust ist eine Systemprogrammiersprache mit dem Fokus auf drei Ziele: Sicherheit, Geschwindigkeit und Nebenläufigkeit (Safety, Speed, Concurrency). Sie erreicht diese Ziele ohne Garbage Collector, was sie zu einer nützlichen Sprache für eine Reihe von Anwendungsfällen macht, in denen andere Sprachen nicht so gut sind: Einbettung in andere Sprachen, Programme mit besonderen Anforderungen an Speicher- oder Zeitbedarf und Schreiben von Low-Level-Code, wie z.B. Gerätetreiber und Betriebssysteme. Sie übertrifft derzeitige Sprachen, die auf diesen Bereich abzielen, indem sie eine Reihe von Sicherheitsprüfungen zur Kompilierzeit durchführt – ohne Kosten zur Laufzeit, indem alle data races vermieden werden. Rust zielt auch darauf ab „kostenfreie Abstraktionen“ zu realisieren, obwohl einige dieser Abstraktionen sich anfühlen wie die einer Hochsprache. Selbst dann erlaubt Rust eine genaue Kontrolle, wie es eine Low-Level-Sprache tun würde.

„Die Programmiersprache Rust“ ist in acht Abschnitte unterteilt. Diese Einführung ist der erste. Danach folgen:

  • Erste Schritte - Richte deinen Computer für die Entwicklung mit Rust ein.
  • Lerne Rust - Lerne Rust-Programmierung durch kleine Projekte.
  • Effektives Rust - Fortgeschrittene Konzepte, um ausgezeichneten Rust-Code zu schreiben.
  • Syntax und Semantik - Jedes Stück Rust auf kleine Stücke heruntergebrochen.
  • Nightly Rust - Cutting-edge features, die noch nicht im stabilen Compiler verfügbar sind.
  • Glossar - Erklärungen von Begriffen, die in diesem Buch verwendet werden.
  • Akademische Forschung - Literatur, die Rust beeinflusst hat.

Nach dem Lesen dieser Einführung möchtest du wahrscheinlich - je nach Vorliebe - entweder ‚Lerne Rust‘ oder ‚Syntax and Semantics‘ lesen: ‚Lerne Rust‘, wenn du mit einem Projekt anfangen möchtest, oder ‚Syntax and Semantics‘, wenn du lieber klein anfangen und jeweils ein einziges Konzept ausführlich lernen möchtest, bevor du mit dem Nächsten weiter machst. Reichliche Querverweise verbinden diese beiden Teile miteinander.

Mithelfen

Dieses Buch ist eine Community-Übersetzung von dem offiziellen Buch „The Rust Programming Language“.

Die Quelldateien dieser Übersetzung befinden sich auf Github: github.com/rust-lang-de/rustbook-de

Die Quelldateien des englischen Originals befinden sich ebenfalls auf Github: github.com/rust-lang/rust/tree/master/src/doc/book

Eine kurze Einführung in Rust

Ist Rust eine Sprache, die dich interessieren könnte? Lass uns ein paar Code-Beispiele anschauen, um ein paar ihrer Stärken zu demonstrieren.

Das Hauptkonzept, das Rust einmalig macht, wird ‚ownership‘ [engl.: Eigentum] genannt. Betrachte dieses kleine Beispiel:

fn main() {
    let mut x = vec!["Hallo", "Welt"];
}

Dieses Programm macht eine Variablenbindung namens x. Der Wert dieser Bindung ist ein Vec<T>, ein ‚Vektor‘, den wir durch ein Makro aus der Standardbibliothek erzeugt haben. Dieses Makro heißt vec und wir rufen Makros mit einem ! auf. Dies folgt einem allgemeinen Prinzip von Rust: Mach Dinge klar! Makros können bedeutend mehr komplizierte Dinge tun als Funktionsaufrufe und damit sind sie optisch eindeutig. Das ! hilft auch beim Parsen, was es erleichtert Werkzeuge zu schreiben und ebenfalls wichtig ist.

Wir haben mut benutzt, um x mutable [engl.: veränderbar] zu machen: Bindungen sind standardmäßig immutable [engl.: unveränderbar]. Wir werden den Vektor noch später in diesem Beispiel verändern.

Es ist ebenfalls beachtenswert, dass hier keine Typangaben notwendig waren: Obwohl Rust statisch typisiert ist, mussten wir den Typ nicht ausdrücklich angeben. Rust hat type inference [engl.: Typinferenz, Typableitung], um die Stärke statischer Typen und der Ausführlichkeit des Angebens von Typen auszubalancieren.

Rust alloziert Daten bevorzugt auf dem Stack als auf dem Heap: x wird direkt auf dem Stack platziert. Der Vec<T> Typ jedoch reserviert Speicher für die Elemente des Vektors auf dem Heap. Falls du nicht mit dieser Unterscheidung vertraut bist, dann kannst du sie fürs Erste ignorieren oder einen Blick in ‚Der Stack und der Heap‘ werfen. Als eine Systemprogrammiersprache gibt Rust dir die Möglichkeit zu bestimmen, wie dein Speicher alloziert wird, aber wenn du gerade erst beginnst, ist das keine so große Sache.

Zuvor haben wir erwähnt, dass ‚ownership‘ das entscheidend neue Konzept in Rust ist. Im Rust-Jargon sagen wir, dass x den Vektor ‚besitzt‘. Dies bedeutet, dass der Speicher des Vektors freigegeben wird, wenn x den Scope [engl.: Geltungsbereich] verlässt. Dieser Vorgang wird deterministisch vom Rust-Compiler vorgenommen - anstatt durch einen Mechanismus wie einen Garbage Collector. Dies bedeutet, dass man in Rust selbst keine Funktionen wie malloc und free aufruft: Der Compiler bestimmt statisch, wann du Speicher allozieren oder freigeben musst und fügt diese Aufrufe selbst ein. Irren ist menschlich, aber Compiler vergessen nie.

Lass uns eine weitere Zeile unserem Beispiel hinzufügen:

fn main() {
    let mut x = vec!["Hallo", "Welt"];

    let y = &x[0];
}

Wir haben eine weitere Variablenbindung y hinzugefügt. In diesem Fall ist y eine ‚Referenz‘ auf das erste Element des Vektors. Rusts Referenzen sind ähnlich wie Zeiger in anderen Sprachen, aber mit zusätzlichen Überprüfungen zur Kompilierzeit. Referenzen interagieren mit dem ownership-System durch das ‚Ausleihen‘ (borrowing) dessen, worauf sie zeigen. Der Unterschied ist, dass sie nicht den zugrunde liegenden Speicher freigibt, wenn die Referenz den Scope verlässt. Falls sie das täte, dann würden wir zweimal freigeben, was schlecht wäre.

Lass uns eine dritte Zeile hinzufügen. Sie schaut harmlos aus, erzeugt aber einen Kompilierfehler.

fn main() {
    let mut x = vec!["Hallo", "Welt"];

    let y = &x[0];

    x.push("foo");
}

push ist eine Methode auf Vektoren, die ein weiteres Element an das Ende des Vektors anhängt. Wenn wir versuchen dieses Programm zu kompilieren, erhalten wir einen Fehler:

error: cannot borrow `x` as mutable because it is also borrowed as immutable
    x.push("foo");
    ^
note: previous borrow of `x` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `x` until the borrow ends
    let y = &x[0];
             ^
note: previous borrow ends here
fn main() {

}
^

Uff! Der Rust-Compiler erzeugt manchmal recht detailierte Fehlermeldungen und dies ist ein solches Mal. Wie der Fehler erklärt, ist zwar unsere Variablenbindung veränderbar, aber wir können immer noch nicht push aufrufen. Das ist so, weil wir bereits eine Referenz auf ein Element des Vektors - nämlich y - haben. Etwas zu verändern, während eine weitere Referenz darauf existiert, ist gefährlich, weil wir die Referenz ungültig machen könnten. In diesem konkreten Fall könnte es sein, dass wir beim Erstellen des Vektors nur Platz für zwei Elemente reserviert haben. Ein drittes hinzuzufügen würde dazu führen, einen neuen Speicherbereich für all diese Elemente zu allozieren, hinüber zu kopieren und den internen Zeiger auf diesen Speicher zu setzen. Das alles funktioniert problemlos. Das Problem ist, dass y nicht aktualisiert werden würde und wir somit einen ‚hängenden Zeiger‘ [engl.: dangling pointer] hätten. Das wäre schlecht. Jegliche Benutzung von y wäre in diesem Fall ein Fehler und somit hat der Compiler diesen für uns abgefangen.

Wie lösen wir also dieses Problem? Es gibt zwei mögliche Lösungsansätze. Der erste ist eine Kopie zu machen, anstatt eine Referenz zu benutzen:

fn main() {
    let mut x = vec!["Hallo", "Welt"];

    let y = x[0].clone();

    x.push("foo");
}

Rust hat standardmäßig Move Semantics, daher rufen wir die clone() Methode auf, wenn wir eine Kopie von irgendwelchen Daten machen wollen. In diesem Beispiel ist y nicht länger eine Referenz auf den Vektor, der in x gespeichert ist, sondern eine Kopie des ersten Elements "Hallo". Nun, da wir keine Referenz haben, funktioniert unser push() einwandfrei.

Wenn wir wirklich eine Referenz haben wollen, dann brauchen wir die andere Option: Sicherstellen, dass unsere Referenzen den Scope verlässt, bevor wir die Veränderung am Vektor vornehmen. Dies sieht so aus:

fn main() {
    let mut x = vec!["Hallo", "Welt"];

    {
        let y = &x[0];
    }

    x.push("foo");
}

Wir haben einen inneren Scope mittels eines weiteren Paars geschweifter Klammern erzeugt. y wird den Scope verlassen, bevor wir push() aufrufen, und damit ist alles in Ordnung.

Dieses Konzept des Besitzes ist nicht nur dazu gut ‚hängende Zeiger‘ zu verhindern, sondern auch eine ganze Reihe verwandter Probleme zu lösen, wie z.B. iterator invalidation, Nebenläufigkeit und mehr.