Rust-Entwicklungsmuster (Patterns)
Idiomatische Rust-Patterns und Best Practices für die Entwicklung sicherer, performanter und wartbarer Anwendungen.
Wann verwenden
- Schreiben von neuem Rust-Code
- Überprüfen von Rust-Code
- Refactoring von bestehendem Rust-Code
- Entwurf von Crate-Struktur und Modul-Layout
Funktionsweise
Dieser Skill erzwingt idiomatische Rust-Konventionen in sechs Schlüsselbereichen: Ownership und Borrowing zur Vermeidung von Datenrennen zur Compile-Zeit, Result/?-Fehlerweitergabe mit thiserror für Bibliotheken und anyhow für Anwendungen, Enums und exhaustives Pattern Matching, um ungültige Zustände nicht darstellbar zu machen, Traits und Generics für nullkostspielige Abstraktion, sichere Nebenläufigkeit über 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 nötig
fn process(data: &[u8]) -> usize {
data.len()
}
// Gut: Ownership nur übernehmen, wenn gespeichert oder verbraucht werden soll
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 Produktion
// Gut: Fehler mit Kontext propagieren
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 Fehler
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: Kombinatorkette
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),
}
}
Exhaustives 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 versteckt neue Varianten
match command {
Command::Start => start_service(),
_ => {} // Ignoriert stillschweigend Stop, Restart und zukünftige Varianten
}
Traits und Generics
Generics akzeptieren, konkrete Typen zurückgeben
// Gut: Generische Eingabe, konkrete Ausgabe
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 Plugin-Systeme nötig sind
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 Vertauschen von Argumenten
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// Kann User- und Order-IDs nicht versehentlich 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 bevorzugen statt manueller Schleifen
// Gut: Deklarativ, lazy, zusammensetzbar
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änderlichen 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 ist in Panik ausgegangen");
}
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 zeitlich ausgelaufen")?
.context("Anfrage fehlgeschlagen")?;
response.text().await.context("Fehler beim Lesen des Rumpfs")
}
// Nebenläufige 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 in Panik ausgegangen: {e}")));
}
results
}
Unsicherer Code (Unsafe)
Wann Unsafe akzeptabel ist
// Akzeptabel: FFI-Grenze mit dokumentierten Invarianten (Rust 2024+)
/// # Sicherheit
/// `ptr` muss ein gültiger, ausgerichteter Zeiger auf einen initialisierten `Widget` 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 Korrektheitsbeweis
// SICHERHEIT: index ist aufgrund der Schleifengrenze immer < len
unsafe { slice.get_unchecked(index) }
Wann Unsafe NICHT akzeptabel ist
// Schlecht: Unsafe zur Umgehung des Borrow-Checkers verwenden
// Schlecht: Unsafe aus Bequemlichkeit verwenden
// Schlecht: Unsafe ohne Safety-Kommentar verwenden
// Schlecht: Zwischen nicht verwandten Typen transmutieren
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
Wesentliche 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 |
| Ungültige Zustände nicht darstellbar machen | Enums verwenden, um nur gültige Zustände zu modellieren |
? statt unwrap() | Fehler propagieren, niemals in Bibliotheks-/Produktionscode panicken |
| Parsen, nicht validieren | Unstrukturierte Daten an der Grenze in typisierte Strukturen umwandeln |
| Newtype für Typsicherheit | Primitive in Newtypes wrappen, um Argumentvertauschungen zu verhindern |
| Iteratoren gegenüber Schleifen bevorzugen | 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 |
| Exhaustives 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() in Produktionscode
let value = map.get("key").unwrap();
// Schlecht: .clone(), um den Borrow-Checker zufriedenzustellen, 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); // Ein 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;
}
Denken Sie daran: Wenn es kompiliert, ist es wahrscheinlich korrekt — aber nur, wenn Sie unwrap() vermeiden, unsafe minimieren und das Typsystem für sich arbeiten lassen.