19 April 2025

Rust-Tutorial: Leitfaden für Anfänger

Rust weckt durchweg das Interesse von Entwicklern und wurde in den Stack Overflow Surveys mehrere Jahre in Folge zur „beliebtesten“ Programmiersprache gekürt. Das ist nicht nur Hype; Rust bietet eine überzeugende Mischung aus Performance, Sicherheit und modernen Sprachmerkmalen, die gängige Schwachstellen anderer Systemprogrammiersprachen adressieren. Wenn du neugierig bist, was Rust besonders macht und deine Reise beginnen möchtest, bietet dieser Einsteigerleitfaden das grundlegende Wissen, um loszulegen. Wir werden die Kernsyntax, einzigartige Konzepte wie Ownership und die essentiellen Werkzeuge erkunden, die das Rust-Ökosystem antreiben.

Warum Rust-Programmierung lernen?

Rust positioniert sich als Sprache zur Entwicklung zuverlässiger und effizienter Software. Seine Hauptvorteile drehen sich um Speichersicherheit ohne Abhängigkeit von einem Garbage Collector und die Ermöglichung von Fearless Concurrency. Hier sind Gründe, warum du erwägen solltest, Rust zu lernen:

  1. Performance: Rust kompiliert direkt zu nativem Maschinencode und bietet eine Performance, die mit C und C++ vergleichbar ist. Es erreicht diese Geschwindigkeit, ohne die Sicherheit zu opfern, was es für leistungskritische Anwendungen wie Spiel-Engines, Betriebssysteme, Browser-Komponenten und Hochleistungs-Webdienste geeignet macht.
  2. Speichersicherheit: Rusts Aushängeschild ist sein **Ownership-System, ergänzt durch *Borrowing und *Lifetimes. Dieses System garantiert Speichersicherheit zur Kompilierzeit. Vergiss hängende Zeiger, Pufferüberläufe oder Datenwettläufe, die Sprachen wie C/C++ oft plagen. Der Rust-Compiler agiert als strenger Wächter und verhindert diese häufigen Fehler, bevor dein Code überhaupt ausgeführt wird.
  3. Nebenläufigkeit: Nebenläufige Programmierung (mehrere Aufgaben scheinbar gleichzeitig ausführen) ist bekanntlich schwierig korrekt umzusetzen. Rust geht dies direkt an. Das Ownership- und Typsystem arbeiten zusammen, um Datenwettläufe zur Kompilierzeit zu verhindern, was das Schreiben von Multi-Threaded-Anwendungen erheblich einfacher und sicherer macht. Diese „Fearless Concurrency“ ermöglicht es Entwicklern, moderne Multi-Core-Prozessoren effektiv zu nutzen, ohne die üblichen Fallstricke.
  4. Moderne Werkzeuge: Rust wird mit Cargo ausgeliefert, einem außergewöhnlichen Paketmanager und Build-Tool, das fest in die Kern-Erfahrung integriert ist. Cargo kümmert sich nahtlos um Abhängigkeitsverwaltung, Testen, Bauen und Veröffentlichen von *Crates (Rusts Begriff für Pakete oder Bibliotheken) und rationalisiert so den gesamten Entwicklungsworkflow.
  5. Wachsendes Ökosystem & Community: Die Rust-Community ist bekannt dafür, lebendig, aktiv und einladend für Neulinge zu sein. Das Ökosystem von Bibliotheken (Crates) erweitert sich rasant und deckt vielfältige Bereiche ab, von Webentwicklung (Frameworks wie Actix, Rocket, Axum) und Netzwerkprogrammierung (wie Tokio für asynchrone Operationen) bis hin zu eingebetteten Systemen, Data Science und Kommandozeilenwerkzeugen.

Rust-Programmierung lernen: Essentielle Werkzeuge

Bevor du deine erste Zeile Rust-Code schreibst, musst du die Rust-Toolchain einrichten. Der standardmäßige und empfohlene Weg zur Installation von Rust ist die Verwendung von rustup, dem Rust-Toolchain-Installer.

  1. rustup: Dieses Kommandozeilen-Tool verwaltet deine Rust-Installationen. Es ermöglicht dir, verschiedene Rust-Versionen (wie Stable-, Beta- oder Nightly-Builds) zu installieren, zu aktualisieren und einfach zwischen ihnen zu wechseln. Besuche die offizielle Rust-Website (https://www.rust-lang.org/tools/install) für Installationsanweisungen spezifisch für dein Betriebssystem.
  2. rustc: Dies ist der Rust-Compiler. Nachdem du deinen Rust-Quellcode in .rs-Dateien geschrieben hast, kompiliert rustc diese in ausführbare Binärdateien oder Bibliotheken, die dein Computer verstehen kann. Obwohl entscheidend, wirst du rustc in deinem täglichen Workflow normalerweise nicht sehr oft direkt aufrufen.
  3. cargo: Dies ist Rusts Build-System und Paketmanager, und es ist das Werkzeug, mit dem du am häufigsten interagieren wirst. Cargo orchestriert viele gängige Entwicklungsaufgaben:
    • Neue Projekte erstellen (cargo new).
    • Dein Projekt bauen (cargo build).
    • Dein Projekt ausführen (cargo run).
    • Automatisierte Tests ausführen (cargo test).
    • Abhängigkeiten verwalten (automatisches Abrufen und Kompilieren externer Bibliotheken oder Crates, die in deiner Cargo.toml-Datei aufgeführt sind).
    • Eigene Crates auf crates.io (der zentralen Rust-Paket-Registry) veröffentlichen, damit andere sie verwenden können.

Für schnelles Experimentieren ohne lokale Installation sind Online-Plattformen wie der offizielle Rust Playground oder integrierte Entwicklungsumgebungen (IDEs) wie Replit ausgezeichnete Optionen.

Dein erstes Rust-Programm: Hallo, Rust!

Beginnen wir mit dem traditionellen „Hallo, Welt!“-Programm, einem Initiationsritus beim Lernen jeder neuen Sprache. Erstelle eine Datei namens main.rs und füge den folgenden Code hinzu:

fn main() {
    // This line prints text to the console
    println!("Hello, Rust!");
}

Um dieses einfache Programm mit den Basiswerkzeugen zu kompilieren und auszuführen:

  1. Kompilieren: Öffne dein Terminal oder deine Eingabeaufforderung, navigiere in das Verzeichnis, das main.rs enthält, und führe aus:
    rustc main.rs
    
    Dieser Befehl ruft den Rust-Compiler (rustc) auf, der eine ausführbare Datei erstellt (z. B. main unter Linux/macOS, main.exe unter Windows).
  2. Ausführen: Führe das kompilierte Programm von deinem Terminal aus:
    ./main
    # Unter Windows: .\main.exe
    

Für alles, was über eine einzelne Datei hinausgeht, ist die Verwendung von Cargo jedoch der Standard und ein viel bequemerer Ansatz:

  1. Ein neues Cargo-Projekt erstellen: Führe in deinem Terminal aus:
    cargo new hello_rust
    cd hello_rust
    
    Cargo erstellt ein neues Verzeichnis namens hello_rust, das ein src-Unterverzeichnis mit main.rs (bereits mit dem „Hello, Rust!“-Code gefüllt) und eine Konfigurationsdatei namens Cargo.toml enthält.
  2. Mit Cargo ausführen: Führe einfach aus:
    cargo run
    
    Cargo übernimmt den Kompilierungsschritt und führt dann das resultierende Programm aus und zeigt „Hello, Rust!“ auf deiner Konsole an.

Schauen wir uns den Code-Schnipsel genauer an:

  • fn main(): Dies definiert die Hauptfunktion. Das fn-Schlüsselwort signalisiert eine Funktionsdeklaration. main ist ein spezieller Funktionsname; es ist der Einstiegspunkt, an dem jedes ausführbare Rust-Programm zu laufen beginnt. Die Klammern () zeigen an, dass diese Funktion keine Eingabeparameter entgegennimmt.
  • {}: Geschweifte Klammern definieren einen Codeblock oder Gültigkeitsbereich (Scope). Aller Code, der zur Funktion gehört, steht innerhalb dieser Klammern.
  • println!("Hello, Rust!");: Diese Zeile führt die Aktion aus, Text auf der Konsole auszugeben.
    • println! ist ein Rust-**Makro*. Makros ähneln Funktionen, haben aber einen wesentlichen Unterschied: Sie enden mit einem Ausrufezeichen !. Makros führen Code-Generierung zur Kompilierzeit durch und bieten mehr Mächtigkeit und Flexibilität als reguläre Funktionen (wie die Verarbeitung einer variablen Anzahl von Argumenten, was println! tut). Das println!-Makro gibt den übergebenen Text auf der Konsole aus und fügt automatisch ein Zeilenumbruchzeichen am Ende hinzu.
    • "Hello, Rust!" ist ein *String-Literal – eine feste Zeichensequenz, die Text darstellt und in doppelten Anführungszeichen eingeschlossen ist.
    • ;: Das Semikolon markiert das Ende der Anweisung. Die meisten Zeilen ausführbaren Rust-Codes (Anweisungen) enden mit einem Semikolon.

Rust-Tutorial für Anfänger: Schlüsselkonzepte

Tauchen wir nun in die fundamentalen Bausteine der Rust-Programmiersprache ein.

Variablen und Mutierbarkeit

Variablen werden verwendet, um Datenwerte zu speichern. In Rust deklarierst du Variablen mit dem let-Schlüsselwort.

let aepfel = 5;
let nachricht = "Nimm fünf";

Ein Kernkonzept in Rust ist, dass Variablen standardmäßig unveränderlich (immutable) sind. Das bedeutet, sobald ein Wert an einen Variablennamen gebunden ist, kannst du diesen Wert später nicht mehr ändern.

let x = 10;
// x = 15; // Diese Zeile verursacht einen Kompilierfehler! Kann unveränderlicher Variable `x` nicht zweimal zuweisen.
println!("Der Wert von x ist: {}", x); // {} ist ein Platzhalter für den Wert von x

Diese standardmäßige Unveränderlichkeit ist eine bewusste Designentscheidung, die dir hilft, sichereren, vorhersagbareren Code zu schreiben, indem sie versehentliche Änderungen von Daten verhindert, was eine häufige Fehlerquelle sein kann. Wenn du eine Variable benötigst, deren Wert sich ändern kann, musst du sie bei der Deklaration explizit mit dem mut-Schlüsselwort als mutierbar markieren.

let mut anzahl = 0; // Deklariere 'anzahl' als mutierbar
println!("Ursprüngliche Anzahl: {}", anzahl);
anzahl = 1; // Dies ist erlaubt, da 'anzahl' mit 'mut' deklariert wurde
println!("Neue Anzahl: {}", anzahl);

Rust erlaubt auch Shadowing (Verdeckung). Du kannst eine neue Variable mit demselben Namen wie eine vorherige Variable innerhalb desselben Gültigkeitsbereichs deklarieren. Die neue Variable „verdeckt“ die alte, was bedeutet, dass nachfolgende Verwendungen des Namens sich auf die neue Variable beziehen. Dies unterscheidet sich von der Mutation, da wir eine völlig neue Variable erstellen, die sogar einen anderen Typ haben kann.

let leerzeichen = "   "; // 'leerzeichen' ist anfangs ein String Slice (&str)
let leerzeichen = leerzeichen.len(); // 'leerzeichen' wird nun durch eine neue Variable verdeckt, die die Länge enthält (eine Ganzzahl, usize)
println!("Anzahl der Leerzeichen: {}", leerzeichen); // Gibt den Ganzzahlwert aus

Basisdatentypen

Rust ist eine statisch typisierte Sprache. Das bedeutet, der Typ jeder Variablen muss dem Compiler zur Kompilierzeit bekannt sein. Rust verfügt jedoch über eine hervorragende Typinferenz. In vielen Situationen musst du den Typ nicht explizit hinschreiben; der Compiler kann ihn oft anhand des Werts und deiner Verwendung herausfinden.

let menge = 10;         // Compiler schließt auf i32 (der Standard-Ganzzahltyp mit Vorzeichen)
let preis = 9.99;       // Compiler schließt auf f64 (der Standard-Gleitkommatyp)
let aktiv = true;      // Compiler schließt auf bool (Boolean)
let initial = 'R';      // Compiler schließt auf char (Zeichen)

Wenn du explizit sein möchtest oder musst (z. B. zur Klarheit oder wenn der Compiler Hilfe benötigt), kannst du Typ-Annotationen verwenden, indem du einen Doppelpunkt : gefolgt vom Typnamen angibst.

let punktestand: i32 = 100;       // Explizit eine 32-Bit-Ganzzahl mit Vorzeichen
let verhaeltnis: f32 = 0.5;       // Explizit eine Gleitkommazahl einfacher Genauigkeit
let ist_fertig: bool = false; // Explizit ein Boolean
let note: char = 'A';      // Explizit ein Zeichen (Unicode-Skalarwert)

Rust hat mehrere eingebaute *skalare Typen (die einzelne Werte repräsentieren):

  • Ganzzahlen: Vorzeichenbehaftete Ganzzahlen (i8, i16, i32, i64, i128, isize) speichern sowohl positive als auch negative ganze Zahlen. Vorzeichenlose Ganzzahlen (u8, u16, u32, u64, u128, usize) speichern nur nicht-negative ganze Zahlen. Die Typen isize und usize hängen von der Architektur des Computers ab (32-Bit oder 64-Bit) und werden hauptsächlich zur Indizierung von Sammlungen verwendet.
  • Gleitkommazahlen: f32 (einfache Genauigkeit) und f64 (doppelte Genauigkeit). Der Standard ist f64.
  • Booleans: Der bool-Typ hat zwei mögliche Werte: true oder false.
  • Zeichen: Der char-Typ repräsentiert einen einzelnen Unicode-Skalarwert (umfassender als nur ASCII-Zeichen), eingeschlossen in einfachen Anführungszeichen (z. B. 'z', 'π', '🚀').

Strings: String vs. &str

Der Umgang mit Text in Rust beinhaltet oft zwei primäre Typen, was für Neulinge verwirrend sein kann:

  1. &str (ausgesprochen „String Slice“): Dies ist eine *unveränderliche Referenz auf eine Sequenz von UTF-8-kodierten Bytes, die irgendwo im Speicher gespeichert sind. String-Literale (wie "Hallo") sind vom Typ &'static str, was bedeutet, dass sie direkt in der Binärdatei des Programms gespeichert sind und für die gesamte Laufzeit des Programms leben. Slices bieten eine *Sicht auf String-Daten, ohne sie zu besitzen. Sie haben eine feste Größe.
  2. String: Dies ist ein *besitzender, *vergrößerbarer, *mutierbarer, UTF-8-kodierter String-Typ. String-Daten werden auf dem *Heap gespeichert, was es ermöglicht, ihre Größe zu ändern. Du verwendest typischerweise String, wenn du String-Daten modifizieren musst oder wenn der String seine Daten besitzen und seine eigene Lebensdauer verwalten muss (oft bei der Rückgabe von Strings aus Funktionen oder deren Speicherung in Structs).
// String-Literal (im Programm-Binary gespeichert, unveränderlich)
let statischer_slice: &'static str = "Ich bin unveränderlich";

// Einen besitzenden, Heap-allozierten String aus einem Literal erstellen
let mut dynamischer_string: String = String::from("Start");

// Den String modifizieren (möglich, da er mutierbar ist und seine Daten besitzt)
dynamischer_string.push_str(" und wachsen");
println!("{}", dynamischer_string); // Ausgabe: Start und wachsen

// Einen String Slice erstellen, der auf einen Teil des Strings verweist
// Dieser Slice borgt Daten von dynamischer_string
let slice_aus_string: &str = &dynamischer_string[0..5]; // Verweist auf "Start"
println!("Slice: {}", slice_aus_string);

Funktionen

Funktionen sind fundamental, um Code in benannte, wiederverwendbare Einheiten zu organisieren. Wir sind bereits der speziellen main-Funktion begegnet.

// Funktionsdefinition
fn gruessen(name: &str) { // Nimmt einen Parameter entgegen: 'name', ein String Slice (&str)
    println!("Hallo, {}!", name);
}

// Funktion, die zwei i32-Parameter nimmt und ein i32 zurückgibt
fn addiere(a: i32, b: i32) -> i32 {
    // In Rust wird der letzte Ausdruck im Funktionsrumpf automatisch zurückgegeben,
    // solange er nicht mit einem Semikolon endet.
    a + b
    // Dies ist äquivalent zu: return a + b;
}

fn main() {
    gruessen("Alice"); // Rufe die 'gruessen'-Funktion auf

    let summe = addiere(5, 3); // Rufe 'addiere' auf, binde den Rückgabewert an 'summe'
    println!("5 + 3 = {}", summe); // Ausgabe: 5 + 3 = 8
}

Wichtige Punkte zu Funktionen:

  • Verwende das fn-Schlüsselwort, um Funktionen zu deklarieren.
  • Gib Parameternamen und deren Typen innerhalb der Klammern an.
  • Gib den Rückgabetyp der Funktion nach einem Pfeil -> an. Wenn eine Funktion keinen Wert zurückgibt, ist ihr Rückgabetyp implizit () (ein leeres Tupel, oft als „Unit-Typ“ bezeichnet).
  • Funktionsrümpfe bestehen aus einer Reihe von *Anweisungen (Statements, die eine Aktion ausführen und normalerweise mit ; enden) und enden optional mit einem *Ausdruck (Expression, etwas, das zu einem Wert ausgewertet wird).
  • Der Wert des letzten Ausdrucks in einem Funktionsblock wird automatisch zurückgegeben, wenn er kein Semikolon hat. Das return-Schlüsselwort kann für explizite, frühe Rückgaben von überall innerhalb der Funktion verwendet werden.

Kontrollfluss

Rust bietet Standard-Kontrollflussstrukturen, um die Reihenfolge festzulegen, in der Code ausgeführt wird:

  • if/else/else if: Wird für bedingte Ausführung verwendet.
let zahl = 6;

if zahl % 4 == 0 {
    println!("Zahl ist durch 4 teilbar");
} else if zahl % 3 == 0 {
    println!("Zahl ist durch 3 teilbar"); // Dieser Zweig wird ausgeführt
} else {
    println!("Zahl ist nicht durch 4 oder 3 teilbar");
}

// Wichtig: 'if' ist in Rust ein Ausdruck, d.h. es wird zu einem Wert ausgewertet.
// Dies erlaubt dir, es direkt in 'let'-Anweisungen zu verwenden.
let bedingung = true;
let wert = if bedingung { 5 } else { 6 }; // wert wird 5 sein
println!("Der Wert ist: {}", wert);
// Hinweis: Beide Zweige des 'if'-Ausdrucks müssen zum gleichen Typ ausgewertet werden.
  • Schleifen: Werden für wiederholte Ausführung verwendet.
    • loop: Erstellt eine Endlosschleife. Du verwendest typischerweise break, um die Schleife zu verlassen, optional mit Rückgabe eines Wertes.
    • while: Läuft, solange eine angegebene Bedingung wahr bleibt.
    • for: Iteriert über die Elemente einer Sammlung oder eines Bereichs. Dies ist der am häufigsten verwendete und oft sicherste Schleifentyp in Rust.
// loop Beispiel
let mut zaehler = 0;
let ergebnis = loop {
    zaehler += 1;
    if zaehler == 10 {
        break zaehler * 2; // Schleife verlassen und 'zaehler * 2' zurückgeben
    }
};
println!("Schleifenergebnis: {}", ergebnis); // Ausgabe: Schleifenergebnis: 20

// while Beispiel
let mut num = 3;
while num != 0 {
    println!("{}!", num);
    num -= 1;
}
println!("ZÜNDUNG!!!");

// for Beispiel (Iteration über ein Array)
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // .iter() erstellt einen Iterator über die Elemente des Arrays
    println!("Der Wert ist: {}", element);
}

// for Beispiel (Iteration über einen Bereich)
// (1..4) erstellt einen Bereich einschließlich 1, 2, 3 (exklusive 4)
// .rev() kehrt den Iterator um
for nummer in (1..4).rev() {
    println!("{}!", nummer); // Gibt 3!, 2!, 1! aus
}
println!("ERNEUTE ZÜNDUNG!!!");

Kommentare

Verwende Kommentare, um Erklärungen und Notizen zu deinem Code hinzuzufügen, die der Compiler ignoriert.

// Dies ist ein einzeiliger Kommentar. Er erstreckt sich bis zum Ende der Zeile.

/*
 * Dies ist ein mehrzeiliger Blockkommentar.
 * Er kann sich über mehrere Zeilen erstrecken und ist nützlich
 * für längere Erklärungen.
 */

 let glueckszahl = 7; // Man kann Kommentare auch ans Ende einer Zeile setzen.

Ownership verstehen: Rusts Kernkonzept

Ownership ist Rusts charakteristischstes und zentralstes Merkmal. Es ist der Mechanismus, der es Rust ermöglicht, Speichersicherheit zur Kompilierzeit zu garantieren, ohne einen Garbage Collector zu benötigen. Das Verständnis von Ownership ist der Schlüssel zum Verständnis von Rust. Es folgt drei Kernregeln:

  1. Besitzer: Jeder Wert in Rust hat eine Variable, die als sein Besitzer (Owner) festgelegt ist.
  2. Ein Besitzer: Es kann zu jedem Zeitpunkt nur einen Besitzer eines bestimmten Wertes geben.
  3. Gültigkeitsbereich & Drop: Wenn die Besitzer-Variable den Gültigkeitsbereich verlässt (z. B. die Funktion, in der sie deklariert wurde, endet), wird der Wert, den sie besitzt, *gedroppt. Das bedeutet, sein Speicher wird automatisch freigegeben.
{ // s ist hier nicht gültig, es ist noch nicht deklariert
    let s = String::from("hallo"); // s ist ab hier gültig;
                                   // s 'besitzt' die String-Daten, die auf dem Heap alloziert wurden.
    // Man kann s hier verwenden
    println!("{}", s);
} // Der Gültigkeitsbereich endet hier. 's' ist nicht länger gültig.
  // Rust ruft automatisch eine spezielle 'drop'-Funktion für den String auf, den 's' besitzt,
  // und gibt dessen Heap-Speicher frei.

Wenn du einen besitzenden Wert (wie einen String, Vec oder einen Struct, der besitzende Typen enthält) einer anderen Variablen zuweist oder ihn per Wert an eine Funktion übergibst, wird der Besitz (*Ownership) verschoben (moved). Die ursprüngliche Variable wird ungültig.

let s1 = String::from("original");
let s2 = s1; // Das Ownership der String-Daten wird von s1 zu s2 VERSCHOBEN (MOVED).
             // s1 gilt danach nicht mehr als gültig.

// println!("s1 ist: {}", s1); // Kompilierfehler! Wert hier nach Verschiebung geborgt.
                             // s1 besitzt die Daten nicht mehr.
println!("s2 ist: {}", s2); // s2 ist nun der Besitzer und gültig.

Dieses Verschiebeverhalten (Move Behavior) verhindert „Double Free“-Fehler, bei denen zwei Variablen versehentlich versuchen könnten, denselben Speicherort freizugeben, wenn sie den Gültigkeitsbereich verlassen. Primitive Typen wie Ganzzahlen, Gleitkommazahlen, Booleans und Zeichen implementieren den Copy-Trait, was bedeutet, dass sie bei Zuweisung oder Übergabe an Funktionen einfach kopiert statt verschoben werden.

Borrowing und Referenzen

Was, wenn du eine Funktion einen Wert verwenden lassen möchtest, ohne das Ownership zu übertragen? Du kannst Referenzen erstellen. Das Erstellen einer Referenz wird Borrowing (Borgen/Leihen) genannt. Eine Referenz erlaubt es dir, auf Daten zuzugreifen, die einer anderen Variablen gehören, ohne das Ownership zu übernehmen.

// Diese Funktion nimmt eine Referenz (&) auf einen String entgegen.
// Sie borgt sich den String, übernimmt aber nicht das Ownership.
fn berechne_laenge(s: &String) -> usize {
    s.len()
} // Hier verlässt s (die Referenz) den Gültigkeitsbereich. Aber da sie nicht die
  // String-Daten besitzt, werden die Daten NICHT gedroppt, wenn die Referenz den Gültigkeitsbereich verlässt.

fn main() {
    let s1 = String::from("hallo");

    // Wir übergeben eine Referenz auf s1 mit dem '&'-Symbol.
    // s1 besitzt weiterhin die String-Daten.
    let laenge = berechne_laenge(&s1);

    // s1 ist hier immer noch gültig, da das Ownership nie verschoben wurde.
    println!("Die Länge von '{}' ist {}.", s1, laenge);
}

Referenzen sind standardmäßig unveränderlich, genau wie Variablen. Wenn du die geborgten Daten modifizieren möchtest, benötigst du eine mutierbare Referenz, gekennzeichnet durch &mut. Rust erzwingt jedoch strenge Regeln für mutierbare Referenzen, um Datenwettläufe (Data Races) zu verhindern:

Die Borrowing-Regeln:

  1. Zu jedem Zeitpunkt darf man entweder haben:
    • Eine mutierbare Referenz (&mut T).
    • Beliebig viele unveränderliche Referenzen (&T).
  2. Referenzen müssen immer gültig sein (sie dürfen die Daten, auf die sie zeigen, nicht überleben – dies wird durch Lifetimes verwaltet, oft implizit).

Diese Regeln werden vom Compiler erzwungen.

// Diese Funktion nimmt eine mutierbare Referenz auf einen String entgegen
fn aendere(ein_string: &mut String) {
    ein_string.push_str(", Welt");
}

fn main() {
    // 's' muss als mutierbar deklariert werden, um mutierbares Borrowing zu erlauben
    let mut s = String::from("hallo");

    // Beispiel für die Durchsetzung der Borrowing-Regeln (Zeilen auskommentieren, um Fehler zu sehen):
    // let r1 = &s; // unveränderliches Borrow - OK
    // let r2 = &s; // weiteres unveränderliches Borrow - OK (mehrere unveränderliche Borrows erlaubt)
    // let r3 = &mut s; // FEHLER! Kann `s` nicht mutierbar borgen, während unveränderliche Borrows aktiv sind
    //                  // Rust erzwingt: entweder mehrere Leser (&T) ODER ein einzelner Schreiber (&mut T), niemals beides
    // println!("{}, {}", r1, r2); // Die Verwendung von r1/r2 hält sie aktiv und löst den Fehler aus
    //                             // Ohne dieses println würde Rusts NLL (Non-Lexical Lifetimes) r1/r2 frühzeitig freigeben,
    //                             // wodurch `&mut s` hier gültig wäre

    // Ein mutierbares Borrow ist hier erlaubt, da keine anderen Borrows aktiv sind
    aendere(&mut s);
    println!("{}", s); // Ausgabe: hallo, Welt
}

Zusammengesetzte Datentypen

Rust bietet Möglichkeiten, mehrere Werte zu komplexeren Typen zu gruppieren.

Structs

Structs (kurz für Strukturen) ermöglichen es dir, benutzerdefinierte Datentypen zu definieren, indem du verwandte Datenfelder unter einem einzigen Namen gruppierst.

// Einen Struct namens User definieren
struct User {
    active: bool,
    username: String, // Verwendet den besitzenden String-Typ
    email: String,
    sign_in_count: u64,
}

fn main() {
    // Eine Instanz des User-Structs erstellen
    // Instanzen müssen Werte für alle Felder angeben
    let mut user1 = User {
        email: String::from("jemand@example.com"),
        username: String::from("einBenutzername123"),
        active: true,
        sign_in_count: 1,
    };

    // Auf Struct-Felder mittels Punktnotation zugreifen
    // Die Instanz muss mutierbar sein, um Feldwerte zu ändern
    user1.email = String::from("andereemail@example.com");

    println!("User E-Mail: {}", user1.email);

    // Verwendung einer Hilfsfunktion zum Erstellen einer User-Instanz
    let user2 = erstelle_user(String::from("user2@test.com"), String::from("user2"));
    println!("User 2 Aktiv-Status: {}", user2.active);

    // Struct-Update-Syntax: Erstellt eine neue Instanz unter Verwendung einiger Felder
    // einer vorhandenen Instanz für die restlichen Felder.
    let user3 = User {
        email: String::from("user3@domain.com"),
        username: String::from("user3"),
        ..user2 // übernimmt die Werte von 'active' und 'sign_in_count' von user2
    };
    println!("User 3 Anmeldeanzahl: {}", user3.sign_in_count);
}

// Funktion, die eine User-Instanz zurückgibt
fn erstelle_user(email: String, username: String) -> User {
    User {
        email, // Feld-Initialisierungs-Kurzschreibweise: wenn Parametername mit Feldname übereinstimmt
        username,
        active: true,
        sign_in_count: 1,
    }
}

Rust unterstützt auch Tupel-Structs, die benannte Tupel sind (z. B. struct Color(i32, i32, i32);), und Unit-ähnliche Structs, die keine Felder haben und nützlich sind, wenn man einen Trait für einen Typ implementieren muss, aber keine Daten speichern muss (z. B. struct AlwaysEqual;).

Enums

Enums (Enumerationen) ermöglichen es dir, einen Typ durch Aufzählung seiner möglichen *Varianten zu definieren. Ein Enum-Wert kann nur eine seiner möglichen Varianten sein.

// Einfaches Enum zur Definition von IP-Adress-Arten
enum IpAddrKind {
    V4, // Variante 1
    V6, // Variante 2
}

// Enum-Varianten können auch zugehörige Daten enthalten
enum IpAddr {
    V4(u8, u8, u8, u8), // V4-Variante enthält vier u8-Werte
    V6(String),         // V6-Variante enthält einen String
}

// Ein sehr häufiges und wichtiges Enum in Rusts Standardbibliothek: Option<T>
// Es kodiert das Konzept eines Wertes, der vorhanden oder abwesend sein kann.
// enum Option<T> {
//     Some(T), // Repräsentiert das Vorhandensein eines Wertes vom Typ T
//     None,    // Repräsentiert das Fehlen eines Wertes
// }

// Ein weiteres entscheidendes Standardbibliotheks-Enum: Result<T, E>
// Wird für Operationen verwendet, die erfolgreich sein (Ok) oder fehlschlagen (Err) können.
// enum Result<T, E> {
//     Ok(T),   // Repräsentiert Erfolg, enthält einen Wert vom Typ T
//     Err(E),  // Repräsentiert Fehlschlag, enthält einen Fehlerwert vom Typ E
// }

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    // Instanzen des IpAddr-Enums mit zugehörigen Daten erstellen
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    // Beispiel mit Option<T>
    let some_number: Option<i32> = Some(5);
    let no_number: Option<i32> = None;

    // Der 'match'-Kontrollflussoperator ist perfekt für die Arbeit mit Enums.
    // Er ermöglicht es, unterschiedlichen Code basierend auf der Enum-Variante auszuführen.
    match some_number {
        Some(i) => println!("Habe eine Zahl bekommen: {}", i), // Wenn es Some ist, binde den inneren Wert an i
        None => println!("Habe nichts bekommen."),           // Wenn es None ist
    }

    // 'match' muss erschöpfend sein: Man muss alle möglichen Varianten behandeln.
    // Der Unterstrich '_' kann als Wildcard-Muster verwendet werden, um alle Varianten
    // abzufangen, die nicht explizit aufgeführt sind.
}

Option<T> und Result<T, E> sind zentral für Rusts Ansatz, potenziell fehlende Werte und Fehler robust zu behandeln.

Einführung in Cargo: Das Rust Build-Tool und der Paketmanager

Cargo ist ein unverzichtbarer Teil des Rust-Ökosystems, der den Prozess des Bauens, Testens und Verwaltens von Rust-Projekten rationalisiert. Es ist eine der Funktionen, die Entwickler oft loben.

  • Cargo.toml: Dies ist die **Manifest*-Datei für dein Rust-Projekt. Sie ist im TOML (Tom's Obvious, Minimal Language) Format geschrieben. Sie enthält wesentliche Metadaten über dein Projekt (wie Name, Version und Autoren) und, entscheidend, listet seine *Abhängigkeiten (Dependencies) auf (andere externe Crates, von denen dein Projekt abhängt).
    [package]
    name = "mein_projekt"
    version = "0.1.0"
    edition = "2021" # Gibt die zu verwendende Rust Edition an (beeinflusst Sprachmerkmale)
    
    # Abhängigkeiten werden unten aufgeführt
    [dependencies]
    # Beispiel: Füge die 'rand'-Crate für Zufallszahlengenerierung hinzu
    # rand = "0.8.5"
    # Beim Bauen wird Cargo 'rand' und dessen Abhängigkeiten herunterladen und kompilieren.
    
  • cargo new <projektname>: Erstellt eine neue Projektstruktur für eine binäre (ausführbare) Anwendung.
  • cargo new --lib <bibliotheksname>: Erstellt eine neue Projektstruktur für eine Bibliothek (Crate, die zur Verwendung durch andere Programme gedacht ist).
  • cargo build: Kompiliert dein Projekt und seine Abhängigkeiten. Standardmäßig erstellt es einen nicht optimierten **Debug*-Build. Die Ausgabe wird im Verzeichnis target/debug/ abgelegt.
  • cargo build --release: Kompiliert dein Projekt mit aktivierten Optimierungen, geeignet für die Verteilung oder Leistungstests. Die Ausgabe wird im Verzeichnis target/release/ abgelegt.
  • cargo run: Kompiliert (falls nötig) und führt dein binäres Projekt aus.
  • cargo check: Überprüft deinen Code schnell auf Kompilierfehler, ohne tatsächlich die endgültige ausführbare Datei zu erzeugen. Dies ist typischerweise viel schneller als cargo build und nützlich während der Entwicklung für schnelles Feedback.
  • cargo test: Führt alle in deinem Projekt definierten Tests aus (normalerweise im src-Verzeichnis oder in einem separaten tests-Verzeichnis).

Wenn du eine Abhängigkeit zu deiner Cargo.toml-Datei hinzufügst und dann einen Befehl wie cargo build oder cargo run ausführst, übernimmt Cargo automatisch das Herunterladen der benötigten Crate (und deren Abhängigkeiten) aus dem zentralen Repository crates.io und kompiliert alles zusammen.

Nächste Schritte auf deiner Rust-Reise

Dieser Einstiegsleitfaden hat die absoluten Grundlagen behandelt, um dich auf den Weg zu bringen. Die Rust-Sprache bietet viele weitere mächtige Funktionen, die du erkunden kannst, während du Fortschritte machst:

  • Fehlerbehandlung: Meisterung des Result-Enums, Fehlerweitergabe mit dem ?-Operator und Definition eigener Fehlertypen.
  • Sammlungen: Effektives Arbeiten mit gängigen Datenstrukturen wie Vec<T> (dynamische Arrays/Vektoren), HashMap<K, V> (Hash-Maps), HashSet<T>, etc.
  • Generics: Schreiben von flexiblem, wiederverwendbarem Code, der mit verschiedenen Datentypen arbeiten kann, unter Verwendung von Typparametern (<T>).
  • Traits: Definieren von gemeinsamem Verhalten und Schnittstellen, die Typen implementieren können (ähnlich wie Interfaces in anderen Sprachen, aber mächtiger).
  • Lifetimes: Verstehen, wie Rust sicherstellt, dass Referenzen immer gültig sind, was manchmal explizite Lifetime-Annotationen ('a) erfordert.
  • Nebenläufigkeit: Erkunden von Threads, Kanälen für Message Passing, gemeinsam genutztem Zustand mit Mutex und Arc und Rusts mächtiger async/await-Syntax für asynchrone Programmierung.
  • Makros: Lernen, eigene Makros für fortgeschrittene Code-Generierung zur Kompilierzeit zu schreiben.
  • Module: Organisation größerer Projekte in logische Einheiten, Steuerung der Sichtbarkeit (öffentlich/privat) von Elementen.
  • Testen: Schreiben effektiver Unit-, Integrations- und Dokumentationstests mit Rusts eingebautem Test-Framework.

Fazit

Rust präsentiert ein einzigartiges und überzeugendes Angebot: die Rohleistung, die man von Low-Level-Sprachen wie C++ erwartet, kombiniert mit starken Sicherheitsgarantien zur Kompilierzeit, die ganze Klassen häufiger Fehler eliminieren, insbesondere im Bereich Speicherverwaltung und Nebenläufigkeit. Während die Lernkurve das Verinnerlichen neuer Konzepte wie Ownership und Borrowing (und das Hinhören auf den manchmal strengen Compiler) beinhaltet, ist die Belohnung Software, die hochgradig zuverlässig, effizient und oft langfristig einfacher zu warten ist. Mit seinen exzellenten Werkzeugen durch Cargo und einer unterstützenden Community ist Rust eine lohnende Sprache zum Lernen.

Fang klein an, experimentiere mit Cargo, nimm die hilfreichen (wenn auch manchmal ausführlichen) Fehlermeldungen des Compilers als Führung an, und du wirst auf dem besten Weg sein, diese mächtige und immer beliebter werdende Sprache zu meistern. Viel Spaß beim Rusten!

In Verbindung stehende Artikel