Rust-Entwicklungsmuster
Idiomatische Rust-Patterns und bewährte Verfahren für sichere, performante und wartbare Anwendungen.
Wann verwenden
- Schreiben von neuem Rust-Code
- Überprüfen von Rust-Code
- Refactoring vorhandenen Rust-Codes
- Entwerfen von Crate-Strukturen und Modul-Layouts
Funktionsweise
Diese Skill setzt idiomatische Rust-Konventionen in sechs Schlüsselbereichen durch: Ownership und Borrowing zur Vermeidung von Datenrennen zur Compile-Zeit, Fehlerweitergabe mit Result/? sowie thiserror für Bibliotheken und anyhow für Anwendungen, Enums und erschöpfendes Pattern Matching, um illegale Zustände nicht darstellbar zu machen, Traits und Generics für Nullkosten-Abstraktion, sichere Nebenläufigkeit via Arc<Mutex<T>>, Channels und async/await, sowie minimale pub-Oberflächen, die nach Domänen organisiert sind.
Kernprinzipien
1. Ownership und Borrowing
Rusts Ownership-System verhindert Datenrennen und Speicherfehler zur Compile-Zeit.
// Gut: Referenzen übergeben, wenn kein Ownership benötigt wird
fn process(data: &[u8]) -> usize {
data.len()
}
// Gut: Ownership nur übernehmen, wenn gespeichert oder konsumiert werden muss
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// Schlecht: Unnötiges Klonen, um den Borrow-Checker zu umgehen
fn process_bad(data: &Vec<u8>) -> usize {
let cloned = data.clone(); // Verschwenderisch — einfach borrowen
cloned.len()
}
Cow für flexibles Ownership
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // Nullkosten, wenn keine Mutation nötig
}
}
Fehlerbehandlung
Result und ? verwenden — niemals unwrap() in der Produktion
// Gut: Fehler mit Kontext weitergeben
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Fehler beim Lesen der Konfiguration von {path}"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("Fehler beim Parsen der Konfiguration von {path}"))?;
Ok(config)
}
// Schlecht: Panic bei Fehlern
fn load_config_bad(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap(); // Panic!
toml::from_str(&content).unwrap()
}
Bibliotheksfehler mit thiserror, Anwendungsfehler mit anyhow
// Bibliothekscode: strukturierte, typisierte Fehler
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("Datensatz nicht gefunden: {id}")]
NotFound { id: String },
#[error("Verbindung fehlgeschlagen")]
Connection(#[from] std::io::Error),
#[error("Ungültige Daten: {0}")]
InvalidData(String),
}
// Anwendungscode: flexible Fehlerbehandlung
use anyhow::{bail, Result};
fn run() -> Result<()> {
let config = load_config("app.toml")?;
if config.workers == 0 {
bail!("Worker-Anzahl muss > 0 sein");
}
Ok(())
}
Option-Kombinatoren statt verschachteltem Matching
// Gut: Kombinator-Kette
fn find_user_email(users: &[User], id: u64) -> Option<String> {
users.iter()
.find(|u| u.id == id)
.map(|u| u.email.clone())
}
// Schlecht: Tief verschachteltes Matching
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
match users.iter().find(|u| u.id == id) {
Some(user) => match &user.email {
email => Some(email.clone()),
},
None => None,
}
}
Enums und Pattern Matching
Zustände als Enums modellieren
// Gut: Unmögliche Zustände sind nicht darstellbar
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
Erschöpfendes Matching — kein Catch-All für Geschäftslogik
// Gut: Jede Variante explizit behandeln
match command {
Command::Start => start_service(),
Command::Stop => stop_service(),
Command::Restart => restart_service(),
// Hinzufügen einer neuen Variante erzwingt Behandlung hier
}
// Schlecht: Wildcard übersieht neue Varianten
match command {
Command::Start => start_service(),
_ => {} // Übersieht stillschweigend Stop, Restart und zukünftige Varianten
}
Traits und Generics
Generics akzeptieren, konkrete Typen zurückgeben
// Gut: Generischer Input, konkreter Output
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
Ok(buf)
}
// Gut: Trait-Bounds für mehrere Einschränkungen
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("verarbeitet: {item}")
}
Trait-Objekte für dynamischen Dispatch
// Verwenden, wenn heterogene Sammlungen oder Pluginsysteme benötigt werden
trait Handler: Send + Sync {
fn handle(&self, request: &Request) -> Response;
}
struct Router {
handlers: Vec<Box<dyn Handler>>,
}
// Generics verwenden, wenn Performance benötigt wird (Monomorphisierung)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
handler.handle(request)
}
Newtype-Pattern für Typsicherheit
// Gut: Unterschiedliche Typen verhindern Verwechslung von Argumenten
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// Kann nicht versehentlich User- und Order-ID vertauschen
todo!()
}
// Schlecht: Leicht, Argumente zu vertauschen
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
todo!()
}
Structs und Datenmodellierung
Builder-Pattern für komplexe Konstruktion
struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
}
}
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
impl ServerConfigBuilder {
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
fn build(self) -> ServerConfig {
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
}
}
// Verwendung: ServerConfig::builder("localhost", 8080).max_connections(200).build()
Iteratoren und Closures
Iterator-Ketten manuellen Schleifen vorziehen
// Gut: Deklarativ, faul, komponierbar
let active_emails: Vec<String> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.clone())
.collect();
// Schlecht: Imperative Akkumulation
let mut active_emails = Vec::new();
for user in &users {
if user.is_active {
active_emails.push(user.email.clone());
}
}
collect() mit Typannotation verwenden
// In verschiedene Typen sammeln
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();
// Results sammeln – bricht beim ersten Fehler ab
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
Nebenläufigkeit
Arc<Mutex<T>> für gemeinsam genutzten, veränderbaren Zustand
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
std::thread::spawn(move || {
let mut num = counter.lock().expect("Mutex vergiftet");
*num += 1;
})
}).collect();
for handle in handles {
handle.join().expect("Worker-Thread panicked");
}
Channels für Nachrichtenaustausch
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(16); // Bounded Channel mit Backpressure
for i in 0..5 {
let tx = tx.clone();
std::thread::spawn(move || {
tx.send(format!("Nachricht {i}")).expect("Empfänger getrennt");
});
}
drop(tx); // Sender schließen, damit rx-Iterator terminiert
for msg in rx {
println!("{msg}");
}
Async mit Tokio
use tokio::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<String> {
let response = tokio::time::timeout(
Duration::from_secs(5),
reqwest::get(url),
)
.await
.context("Anfrage zeitüberschreitung")?
.context("Anfrage fehlgeschlagen")?;
response.text().await.context("Fehler beim Lesen des Bodys")
}
// Gleichzeitige Aufgaben starten
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
let handles: Vec<_> = urls.into_iter()
.map(|url| tokio::spawn(async move {
fetch_with_timeout(&url).await
}))
.collect();
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
results.push(handle.await.unwrap_or_else(|e| panic!("Gestartete Aufgabe panicked: {e}")));
}
results
}
Unsicherer Code
Wann Unsafe akzeptabel ist
// Akzeptabel: FFI-Grenze mit dokumentierten Invarianten (Rust 2024+)
/// # Sicherheit
/// `ptr` muss ein gültiger, ausgerichteter Zeiger auf eine initialisierte `Widget`-Instanz sein.
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
// SICHERHEIT: Aufrufer garantiert, dass ptr gültig und ausgerichtet ist
unsafe { &*ptr }
}
// Akzeptabel: Performance-kritischer Pfad mit Korrektheitsnachweis
// SICHERHEIT: index ist aufgrund der Schleifenbegrenzung immer < len
unsafe { slice.get_unchecked(index) }
Wann Unsafe NICHT akzeptabel ist
// Schlecht: Unsafe verwenden, um den Borrow-Checker zu umgehen
// Schlecht: Unsafe aus Bequemlichkeit verwenden
// Schlecht: Unsafe ohne Sicherheitskommentar verwenden
// Schlecht: Zwischen nicht verwandten Typen transmuten
Modulsystem und Crate-Struktur
Nach Domäne organisieren, nicht nach Typ
my_app/
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── auth/ # Domänenmodul
│ │ ├── mod.rs
│ │ ├── token.rs
│ │ └── middleware.rs
│ ├── orders/ # Domänenmodul
│ │ ├── mod.rs
│ │ ├── model.rs
│ │ └── service.rs
│ └── db/ # Infrastruktur
│ ├── mod.rs
│ └── pool.rs
├── tests/ # Integrationstests
├── benches/ # Benchmarks
└── Cargo.toml
Sichtbarkeit — Minimal exponieren
// Gut: pub(crate) für internes Teilen
pub(crate) fn validate_input(input: &str) -> bool {
!input.is_empty()
}
// Gut: Öffentliche API aus lib.rs re-exportieren
pub mod auth;
pub use auth::AuthMiddleware;
// Schlecht: Alles pub machen
pub fn internal_helper() {} // Sollte pub(crate) oder privat sein
Tooling-Integration
Wichtige Befehle
# Bauen und Prüfen
cargo build
cargo check # Schnelle Typprüfung ohne Codegenerierung
cargo clippy # Lints und Vorschläge
cargo fmt # Code formatieren
# Testen
cargo test
cargo test -- --nocapture # println-Ausgabe anzeigen
cargo test --lib # Nur Unit-Tests
cargo test --test integration # Nur Integrationstests
# Abhängigkeiten
cargo audit # Sicherheitsaudit
cargo tree # Abhängigkeitsbaum
cargo update # Abhängigkeiten aktualisieren
# Performance
cargo bench # Benchmarks ausführen
Kurzreferenz: Rust-Idiome
| Idiom | Beschreibung |
|---|---|
| Borrowen, nicht klonen | &T übergeben statt zu klonen, es sei denn, Ownership wird benötigt |
| Illegale Zustände nicht darstellbar machen | Enums verwenden, um nur gültige Zustände zu modellieren |
? statt unwrap() | Fehler weitergeben, niemals in Bibliotheks-/Produktionscode panicen |
| Parsen, nicht validieren | Unstrukturierte Daten an der Grenze in typisierte Structs umwandeln |
| Newtype für Typsicherheit | Primitive in Newtypes verpacken, um Argumentvertauschungen zu verhindern |
| Iteratoren Schleifen vorziehen | Deklarative Ketten sind klarer und oft schneller |
#[must_use] auf Results | Sicherstellen, dass Aufrufer Rückgabewerte behandeln |
Cow für flexibles Ownership | Allokationen vermeiden, wenn Borrowing ausreicht |
| Erschöpfendes Matching | Kein Wildcard _ für geschäftskritische Enums |
Minimale pub-Oberfläche | pub(crate) für interne APIs verwenden |
Anti-Patterns, die vermieden werden sollten
// Schlecht: .unwrap() im Produktionscode
let value = map.get("key").unwrap();
// Schlecht: .clone(), um den Borrow-Checker zu besänftigen, ohne zu verstehen, warum
let data = expensive_data.clone();
process(&original, &data);
// Schlecht: String verwenden, wenn &str ausreicht
fn greet(name: String) { /* sollte &str sein */ }
// Schlecht: Box<dyn Error> in Bibliotheken (stattdessen thiserror verwenden)
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
// Schlecht: must_use-Warnungen ignorieren
let _ = validate(input); // Ergebnis eines Result stillschweigend verwerfen
// Schlecht: Blockieren im Async-Kontext
async fn bad_async() {
std::thread::sleep(Duration::from_secs(1)); // Blockiert den Executor!
// Verwenden: tokio::time::sleep(Duration::from_secs(1)).await;
}
Denk daran: Wenn es compiliert, ist es wahrscheinlich korrekt — aber nur, wenn du unwrap() vermeidest, unsafe minimierst und das Typsystem für dich arbeiten lässt.