Karriere
Wissen
Über uns
13. Okt 2025
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.
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.
Bevor wir tiefer in Rust einsteigen, ist es wichtig, zwei grundlegende Speicherbereiche zu verstehen:
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 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.
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.
Um die Fehlerbehandlung in Rust besser zu verstehen, müssen wir uns zunächst anschauen, wie Enums funktionieren.
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.
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 (?) 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.
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.
Crates sind externe Rust-Bibliotheken. Für Fehlerbehandlung gibt es nützliche Crates:
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.
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.
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 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:
Beliebte Crates sind:
Ein typisches Rust-Projekt, das mit cargo new oder cargo init erstellt wurde, besitzt folgende Elemente:
Innerhalb von src/ können dann entsprechende Module und ausführbare Programme abgelegt werden. main.rs dient dabei als Einstiegspunkt für das Programm.
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.
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:
Danke für Ihr Interesse an Cudos. Ihre Angaben werden vertraulich behandelt – den Newsletter können Sie jederzeit abbestellen.