Bild von Rust – Moderne Softwareentwicklung mit Sicherheit und Performance

13. Okt 2025

Rust – Moderne Softwareentwicklung mit Sicherheit und Performance

Rust vereint Systemsprachen-Performance mit hoher Sicherheit: Wir erklären Ownership, Traits, Fehlerbehandlung und Nebenläufigkeit, stellen Cargo vor und zeigen, wo Rust überzeugt – inklusive Tipps für den Einstieg.

Rust ist eine moderne Programmiersprache, die durch ihre Speicher- und Thread-Sicherheit, hohe Performance und fortschrittliche Features überzeugt. In diesem Beitrag beleuchten wir, warum Rust besonders für anspruchsvolle Anwendungsgebiete – von Embedded über Cloud bis Desktop – interessant ist und was die Sprache ausmacht.

Bereits vor zwei Jahren war Rainer Stropek bei uns zu Gast und hat Rust eingeführt. Im entsprechenden Blogbeitrag  Robuste und fehlerfreie Softwareentwicklung mit Rust  finden sich bereits alles Wichtige zur Rust-Syntax, Ownership und Borrowing. Dieser Beitrag baut darauf auf und behandelt ausführlicher Traits, Fehlerbehandlung, asynchrone Programmierung sowie den Paketmanager Cargo. 

Warum Rust?

In der Softwareentwicklung sind Performance, Sicherheit und Wartbarkeit zentrale Herausforderungen. Rust bietet eine einzigartige Kombination aus der Geschwindigkeit von C/C++ und moderner Benutzerfreundlichkeit – ohne Kompromisse bei der Sicherheit. Dank seines innovativen Speicherverwaltungssystems kann Rust in Benchmarks die gleiche oder sogar eine höhere Performance als C/C++ erreichen, während typische Fehler wie Speicher- oder Thread-Sicherheitsprobleme vermieden werden. 

Speicherverwaltung: Stack und Heap

Bevor wir tiefer in Rust einsteigen, ist es wichtig, zwei grundlegende Speicherbereiche zu verstehen: 

  • Stack: Ein schneller Speicher für kleine, zur Compilezeit bekannte Daten. Der Speicher wird automatisch verwaltet nach dem "Last In, First Out"-Prinzip.
  • Heap: Ein flexibler Speicher für grössere oder dynamische Daten. Der Speicherzugriff ist zwar langsamer, dafür hat man mehr Kontrolle über die Lebensdauer der einzelnen Objekte.

Komplexere Typen wie String oder Vec<T> werden auf dem Heap gespeichert, da sie zur Kompilierzeit keine bekannte Grösse haben. Im Stack liegen nur einige Informationen, wie zum Beispiel ein Pointer zum entsprechenden Speicherbereich im Heap und die allozierte Grösse. 

Traits – Rusts Interface-System

Traits sind Rusts Version von Interfaces. Sie definieren gemeinsames Verhalten (Methodensignaturen), das verschiedene Typen implementieren können: 

trait Billable {
    fn total(&self) -> f32;
}
struct ConsultingWork {
    hours: f32,
    rate: f32,
}
impl Billable for ConsultingWork {
    fn total(&self) -> f32 {
        self.hours * self.rate
    }
}
fn print_billable<T: Billable>(item: &T) {
    println!("Gesamtbetrag: {:.2} €", item.total());
}
fn main() {
    let consulting = ConsultingWork { hours: 10.0, rate: 50.0 };
    print_billable(&consulting);
}

print_billable kann dabei jedes Objekt übergeben werden, welches den Billable Trait implementiert.

Derive-Makros und automatische Trait-Implementierung

Mit sogenannten Derive-Makros können einige Standard-Traits automatisch für Structs generiert werden: 

#[derive(Clone, Copy, Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

Primitive und kleine Typen mit dem Copy-Trait können effizient kopiert werden. Beispielhaft anhand eines 32-bit-Integer: 

let i: i32 = 42;
let j = i; // 'i' bleibt weiterhin nutzbar, da i32 das Copy-Trait implementiert

Komplexere Typen, zum Beispiel solche, die Heap-Daten benutzen, können in Rust nicht den Copy-Trait implementieren. Das liegt daran, dass beim Kopieren nur die im Stack gespeicherten Teile (wie Pointer auf den Heap, Länge, Kapazität usw.) dupliziert würden – nicht aber die eigentlichen Daten im Heap. Dies wäre unsicher, da dann mehrere Variablen auf denselben Speicherbereich zeigen und beim Aufräumen Speicherfehler entstehen könnten. Rust erkennt solche Fälle beim Kompilieren und gibt einen Fehler aus. Für solche Typen ist stattdessen der Clone-Trait vorgesehen, der auch die Heap-Daten korrekt dupliziert. 

Fehlerbehandlung

Um die Fehlerbehandlung in Rust besser zu verstehen, müssen wir uns zunächst anschauen, wie Enums funktionieren. 

Enums

Rust-Enums sind mächtiger als klassische Aufzählungen und können Daten beinhalten: 

enum HotelRoom {
    Vacant,
    Occupied(String),
    Maintenance(MaintenanceWork), // MaintenanceWork ist ein struct
    NotUsable,
}

Sie eignen sich perfekt für Pattern Matching mit match

match room {
    HotelRoom::Vacant => println!("Zimmer ist frei"),
    HotelRoom::Occupied(guest) => println!("Belegt von: {}", guest),
    HotelRoom::Maintenance(work) => println!("Wartung: {:?}", work),
    _ => println!("Kann nicht benutzt werden"),
}

Rust erzwingt beim Kompilieren, dass im Match-Statement der gesamte Wertebereich des Enums abgedeckt ist. Der _ Operator deckt dabei alle Werte ab, die noch nicht anderweitig abgefragt wurden. 

Result & Option

Statt Exceptions verwendet Rust Result<T, E> und Option<T> als Rückgabewerte für sichere Fehlerbehandlung. Beides sind Enums, wie im vorherigen Kapitel beschrieben. Sie sind folgendermassen definiert: 

enum Result<T, E> {
   Ok(T),
   Err(E),
}
enum Option<T> {
    Some(T),
    None,
}

T und E sind dabei die Datentypen für das entsprechende Feld im Enum. Sie werden zum Beispiel wie folgt verwendet: 

fn divide_result(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division durch Null".to_string())
    } else {
        Ok(a / b)
    }
}
fn divide_option(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

Beide Methoden implementieren die gleiche Logik. Im Fall von Result wird ein Enum vom Typ Result<f64, String> zurückgegeben, also ein 64-bit float im Erfolgsfall, oder ein String im Fehlerfall. Bei der Variante mit Option wird bei einem Fehler None als Wert zurückgegeben. Der Divisionsoperator (a / b) gibt in Rust immer einen Wert zurück, kein Result oder Option. Daher muss das entsprechende Ergebnis zuerst in den gewünschten Typ umgewandelt werden, bevor es zurückgegeben werden kann. 

Der Fragezeichen-Operator

Der Fragezeichen-Operator (?) vereinfacht das Error-Handling erheblich. Anstatt per match alle Fälle zu behandeln, können Fehler so automatisch an die aufrufende Funktion weitergegeben werden: 

fn calculate_something() -> Result<f64, String> {
    let result = divide(10.0, 2.0)?;
    // Weitere Logik
    Ok(result)
}

Um zu verstehen, wie das funktioniert, lohnt es sich, den äquivalenten Code ohne ?-Operator anzuschauen: 

fn calculate_something() -> Result<f64, String> {
    let result = match divide_result(10.0, 2.0) {
        Ok(val) => val,
        Err(err) => return Err(err),
    };
    // Weitere Logik
    Ok(result)
}

Im Fehlerfall wird der Fehler direkt zurückgegeben. Im Ok-Fall enthält result den Wert der Division. Zum Zurückgeben eines Werts muss dieser dann wieder in den entsprechenden Enum-Typ (hier Ok) verpackt werden. 

Panic!

Für kritische Fehlerfälle, in denen das Programm nicht mehr sinnvoll fortgesetzt werden kann, kann mit   

panic!("Kritischer Fehler")

das Programm beendet werden. Dabei wird auch ein Stacktrace ausgegeben. 

Hilfs-Crates für Fehlerbehandlung

Crates sind externe Rust-Bibliotheken. Für Fehlerbehandlung gibt es nützliche Crates: 

  • anyhow : Für Fehler, bei denen lediglich eine textuelle Beschreibung ausreicht, kann mit anyhow einfach eine entsprechende Fehlermeldung zurückgegeben werden, wie in unserem divide_result 
    Beispiel oben. Geeignet für einfache, allgemeine Fehlerbehandlung.
  • thiserror : Damit können eigene Fehlertypen definiert und zurückgegeben werden. Dies eignet sich für präzise, typisierte Fehler mit eigenen Enum-Strukturen.

Ein tieferer Einstieg in diese Crates würde den Rahmen dieses Beitrags sprengen. Die verlinkte Dokumentation ist ein guter Einstiegspunkt, um mehr darüber zu erfahren. 

Asynchrone Programmierung

Rust bietet keine eigene asynchrone Laufzeitumgebung im Compiler. Hat man jedoch ein unterstützendes Betriebssystem, kann man tokio.rs  für asynchronen Code einsetzen: 

async fn say_world() {
    println!("world");
}
#[tokio::main]
async fn main() {
    let op = say_world();
    print!("hello ");
    op.await;
}
// Gibt "hello world" aus

Die Funktion say_world hat keinen expliziten Rückgabetyp. Sie liefert einen Typ zurück, der das Trait Future implementiert. await ist ein Keyword von Rust und wird an Methodenaufrufe angehängt. Im Beispiel wird say_world erst durch .await ausgeführt. 

Projektorganisation mit Cargo

Cargo ist das Standard-Build-Tool und der Paketmanager von Rust. Es erleichtert die Verwaltung, den Bau und das Testen von Rust-Projekten sowie den Umgang mit Abhängigkeiten aus der Open-Source-Community. 

Externe Abhängigkeiten

Externe Bibliotheken (genannt Crates) werden in der Datei Cargo.toml eingetragen. Beispiel für den Eintrag zweier Crates mit spezifischen Features: 

[dependencies]
serde = { version = "1.0" }
tokio = { version = "1.0" }

Abhängigkeiten lassen sich komfortabel über die Kommandozeile hinzufügen oder entfernen: 

  • Hinzufügen: cargo add serde
  • Entfernen: cargo remove serde

Beliebte Crates sind: 

  • serde: Serialisierung & Deserialisierung, z. B. von JSON
  • tokio: Asynchrone Laufzeit für Netzwerk und I/O
  • clap: Parsing von Kommandozeilenargumenten
Projektstruktur

Ein typisches Rust-Projekt, das mit cargo new oder cargo init erstellt wurde, besitzt folgende Elemente: 

  • src/: Quellcode-Verzeichnis
  • Cargo.toml: Zentrale Konfigurationsdatei (Projektmetadaten & Abhängigkeiten)
  • Cargo.lock: (automatisch generiert) Hält exakte Abhängigkeitsversionen fest

Innerhalb von src/ können dann entsprechende Module und ausführbare Programme abgelegt werden. main.rs dient dabei als Einstiegspunkt für das Programm.

Modulsystem und Sichtbarkeit

Für grössere Projekte kann der Quellcode auch in mehrere Dateien aufgeteilt werden. Dafür organisiert Rust den Code in Modulen, die gezielt freigegeben werden. Ein Beispiel für ein Modul in der Datei src/math_utils.rs ist: 

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Mit pub macht man Funktionen, Typen oder andere Module öffentlich und somit für andere Module zugänglich. 

In einer anderen Datei (z.B. src/main.rs) kann dieses Modul dann wie folgt eingebunden werden: 

mod math_utils;

fn main() {
    let result = math_utils::add(3, 4);
    println!("{}", result);
}

Die mod math_utils; Deklaration teilt dem Compiler mit, dass er nach einer Datei math_utils.rs im src/ Verzeichnis suchen soll. Die Rust Dokumentation hat eine gute Anleitung, wie ein Package mit Cargo aufgebaut werden kann.

Fazit

Rust kombiniert Performance und Sicherheit auf einzigartige Weise. Auch das Ökosystem überzeugt – so ist Rust seit mehreren Jahren die meistgeschätzte Sprache im Stack Overflow Survey. Die Sprache ist allerdings recht komplex mit einer hohen Lernkurve und sollte daher für Anwendungen, bei denen Performance nicht kritisch ist, nicht die erste Wahl sein. Bereiche, in denen Rust jedoch hervorragend geeignet ist, sind unter anderem: 

  • Embedded Systems: Durch Speichersicherheit ohne Runtime-Overhead ist Rust ideal für Mikrocontroller.
  • Sicherheitskritische Anwendungen: Dank Memory-Safety können Sicherheitslücken verhindert werden.
  • Cloud und Backend: Für Server mit hohem Bedarf an Performance und Sicherheit.Für Anwendungen mit grafischer Oberfläche kann für Desktop-Anwendungen ein Framework wie Tauri genutzt werden, das eine Electron-Alternative für Rust darstellt. Im Bereich Webanwendungen kann Rust im Backend verwendet werden; im Frontend kann eine klassische Webtechnologie oder beispielsweise Flutter für plattformübergreifende Entwicklung genutzt werden. 

Links und Ressourcen


Schliessen
Stamp Icon-Print Icon-Clear
S
M
L
XL
XXL