Rust-Entwicklung – Tag 1
Eine praktische Grundlage, um Rust-Apps ab dem ersten Commit gut zu schreiben. Kein Lehrbuch. Konzentriert sich auf die Unterschiede zu anderen Sprachen, die Entscheidungen am ersten Tag, die alles andere prägen, und die kleine Menge an Crates, die die meisten realen Anwendungen abdecken.
Wann verwenden
- Starten eines neuen Rust-Projekts (CLI, Dienst, Bibliothek)
- Wechsel zu Rust von Python, JavaScript, Go, Java/C# oder C++
- Wahl zwischen Owned/Borrowed-Typen, Smart Pointern, Trait-Objekten vs Generics
- Auswahl der Fehlerbehandlungsstrategie (
anyhowvsthiserror) - Entscheidung, welche Crates verwendet werden sollen
- Konfigurieren einer minimalen aber meinungsstarken
Cargo.toml, clippy und rustfmt
Einrichtung am ersten Tag
# 1. Toolchain installieren (rustup ist der Toolchain-Manager)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Komponenten bestätigen (rustfmt und clippy sind im stabilen Kanal enthalten, rust-src ermöglicht IDE-Funktionen)
rustup component add rustfmt clippy rust-src
# 3. Projekt erstellen
cargo new my-app # Binary (src/main.rs)
cargo new --lib my-lib # Bibliothek (src/lib.rs)
# 4. Die Entwicklungsschleife (diese vier einprägen)
cargo check # schnelle Typprüfung, keine Codegenerierung
cargo run # bauen und ausführen (Binary)
cargo test # bauen und Tests ausführen (inkl. Doctests)
cargo clippy --all-targets # Lint, inkl. Tests/ und Benches/ (vor dem Pushen ausführen)
cargo fmt # formatieren
# 5. Abhängigkeiten verwalten, ohne Cargo.toml von Hand zu bearbeiten
cargo add tokio --features full
cargo remove tokio
cargo update # Cargo.lock innerhalb der vorhandenen Semver-Bereiche neu berechnen
cargo update bewegt sich nur innerhalb der in Cargo.toml bereits angegebenen Versionbereiche. Ein Überschreiten einer Hauptversion (1.x auf 2.0) erfordert eine Bearbeitung von Cargo.toml oder cargo add <crate>@2.
rust-analyzer ist zwingend erforderlich. Es ist der Sprachserver, den jeder Editor verwendet (VS Code, Zed, Neovim, Helix, RustRover verwendet eine eigene Engine, ist aber vergleichbar). Installieren Sie in VS Code die Erweiterung rust-analyzer und setzen Sie rust-analyzer.check.command auf "clippy", um Lint-Feedback beim Speichern zu erhalten.
Möchten Sie später einen Datei-Watcher? cargo install --locked bacon, dann führen Sie bacon in Ihrem Projekt aus. Am ersten Tag noch nicht nötig.
Das Rust-Mentalmodell in 5 Ideen
Rust tauscht zwei Dinge, die Sie in den meisten Sprachen als selbstverständlich ansehen (einen Garbage Collector und Exceptions), gegen Garantien zur Compile-Zeit bezüglich Speicher, Datenrennen und Fehlerbehandlung. Die Form der Sprache folgt aus diesem Tausch.
1. Ownership: Jeder Wert hat genau einen Besitzer
Stellen Sie sich Werte wie physische Objekte vor. Ein Buch, eine Datei, eine Netzwerkverbindung. Zu jedem Zeitpunkt besitzt eine Variable es. Sie können:
- Es verschieben:
let b = a;übergibt den Besitz anb.aist weg. - Es unveränderlich ausleihen:
&aerlaubt anderen, es anzusehen. Viele Leser erlaubt. - Es veränderlich ausleihen:
&mut aerlaubt einer Person, es zu ändern. Exklusiver Zugriff. - Es klonen:
a.clone()erstellt eine tiefe Kopie. Beide behalten ihre eigene.
Wenn der Besitzer den Gültigkeitsbereich verlässt, wird der Wert freigegeben (Speicher freigegeben, Datei geschlossen, Sperre aufgehoben). Kein GC, kein manuelles free. Dies ist RAII, vom Compiler erzwungen.
2. Aliasing XOR Mutability
Zu jedem Zeitpunkt hat ein Datum entweder:
- eine veränderliche Referenz (
&mut T), oder - eine beliebige Anzahl unveränderlicher Referenzen (
&T),
niemals beides. Diese eine Regel eliminiert Datenrennen und die meisten Use-After-Free-Fehler. Der Borrow-Checker erzwingt sie. Wenn er sich beschwert, sagt er Ihnen, dass Ihre Datenbesitzgeschichte unklar ist, nicht dass die Sprache schwierig ist.
3. Fehler sind Werte, keine Ausnahmen
Es gibt kein try/catch. Funktionen, die fehlschlagen können, geben Result<T, E> zurück. Funktionen, die nichts Nützliches zurückgeben, geben Option<T> zurück. Der Compiler zwingt Sie, beide zu behandeln. Der ?-Operator propagiert Fehler mit einem Zeichen den Aufrufstapel hinauf:
fn read_config() -> Result<Config, anyhow::Error> {
let text = std::fs::read_to_string("config.toml")?; // ? = frühe Rückkehr bei Err
let config = toml::from_str(&text)?;
Ok(config)
}
Es gibt kein null. Option<T> ist None oder Some(value). Der Compiler lässt Sie den None-Fall nicht vergessen.
4. Traits sind keine Java-Interfaces
Ein trait definiert Verhalten. Typen impl-mentieren Traits. Soweit bekannt. Die Unterschiede:
- Statische Dispatch ist der Standard. Wenn Sie
fn f<T: Display>(x: T)schreiben, generiert der Compiler eine separate Kopie vonffür jeden konkretenT, mit dem Sie es aufrufen (Monomorphisierung, wie C++-Templates). Kein Laufzeit-Overhead. - Dynamischer Dispatch ist opt-in über
dyn Trait(typischerweiseBox<dyn Trait>oder&dyn Trait). Eine Vtable-Lookup pro Aufruf. - Keine Vererbung. Traits komponieren. Wenn Sie nach
Derefgreifen, um einen Typ zu "erweitern", hören Sie auf und verwenden Sie Komposition oder ein Enum. - Waisenregel: Sie können
YourTrait for SomeoneElsesTypeoderSomeoneElsesTrait for YourTypeimplementieren, aber nicht beide fremd. Dies hält die Abhängigkeitsauflösung vernünftig.
5. Der Borrow-Checker ist ein Design-Orakel
Der häufigste Fehler von Neulingen ist, Compiler-Fehler als Hindernisse zu betrachten, die es zu beschwichtigen gilt. Das sind sie nicht. Fast jeder Borrow-Check-Fehler offenbart ein echtes Problem mit wem was gehört. Wenn Sie nicht weiterkommen, ist die Frage selten "wie bringe ich das zum Kompilieren" und fast immer "welche Besitzbeziehung will ich hier eigentlich?" Lesen Sie den Fehler. Der Compiler ist ungewöhnlich informativ.
Die 3 Fragen für jede Funktionssignatur
Bevor Sie eine Funktion schreiben, fragen Sie: Muss sie die Eingabe besitzen, lesen oder ändern?
fn consume(s: String) // besitzt: Funktion übernimmt Verantwortung, Aufrufer verliert sie
fn read(s: &str) // liest: Funktion sieht es an, Aufrufer behält es
fn modify(s: &mut String) // ändert: Funktion ändert es an Ort und Stelle
Standardwerte, die in 90% der Fälle funktionieren:
- Funktionsparameter: bevorzugen Sie
&strgegenüberString,&[T]gegenüberVec<T>(das sind Slices, akzeptieren sowohl Owned- als auch Borrowed-Aufrufer). - Funktionsrückgaben: geben Sie Owned-Typen zurück (
String,Vec<T>). Referenzen zurückzugeben bedeutet Lifetimes; vermeiden Sie dies, bis Sie sie benötigen. - Strukturfelder: bevorzugen Sie Owned-Typen (
String,Vec<T>). Das Speichern von&strin einer Struktur ist die häufigste Neulingsfalle und kaskadiert Lifetime-Annotationen durch jeden Typ, der Ihre Struktur hält.
Entscheidungstabelle für den ersten Tag
Einzeilige Antworten auf die Entscheidungen, die zuerst auftauchen.
| Entscheidung | Standard | Wann das andere wählen |
|---|---|---|
String vs &str (Strukturfeld) | String | Fast nie &str, bis Sie einen echten Grund haben und Lifetimes verstehen |
String vs &str (Funktionsparam) | &str | Verwenden Sie String nur, wenn Sie es im Inneren besitzen/speichern müssen |
Vec<T> vs &[T] (Param) | &[T] | Vec<T> nur, wenn Sie besitzen müssen |
Box<T> vs Rc<T> vs Arc<T> | Box<T> (einzelner Besitzer, Heap) | Arc<T> für gemeinsamen Besitz über Threads hinweg. Vermeiden Sie Rc<T> als Standard; verwenden Sie Arc<T>, damit Sie nicht umgestalten müssen, wenn Sie async gehen |
RefCell<T> vs Mutex<T> | Mutex<T> (oder RwLock<T>) | Gleicher Grund: funktioniert in async/Threads, während RefCell dies nicht tut |
Option<T> vs Result<T, E> | Option<T> für "kein Wert", Result<T, E> für "aus einem Grund fehlgeschlagen" | Wenn das Fehlen eine Bedeutung trägt, die der Aufrufer behandeln sollte, Result |
dyn Trait vs impl Trait / <T: Trait> | Generisch (<T: Trait> oder impl Trait) - statischer Dispatch | Box<dyn Trait>, wenn Sie eine heterogene Sammlung benötigen (Vec<Box<dyn Animal>>) |
| Fehler in Anwendungscode | anyhow::Result<T> überall | - |
| Fehler in Bibliothekscode | thiserror-abgeleitetes Enum | Niemals Box<dyn Error> in öffentlichen Bibliotheks-APIs – zwingt Aufrufer zum Downcast |
&self vs &mut self vs self | &self für Getter, &mut self für Setter, self für Builder/verbrauchende Operationen | - |
| Modulanordnung | Inline-Module, bis eine Datei lang wird, dann aufteilen | Ein Modul = eine Datei ist ein Java/C#-Instinkt, kein Rust-Instinkt |
Redewendungen, die Sie früh verinnerlichen sollten
Diese tauchen in fast jedem Rust-Programm auf. Lernen Sie sie in Woche 1.
? zur Fehlerweitergabe. Ersetzt neun Zeilen match mit einem Zeichen.
let body = reqwest::get(url).await?.text().await?;
Iterator-Ketten statt manueller Schleifen. Kompilieren zum gleichen Maschinencode wie handgeschriebene Schleifen (LLVM inline Closures). Idiomatisches Rust ist funktional im Stil.
let active_emails: Vec<String> = users
.iter()
.filter(|u| u.active)
.map(|u| u.email.clone())
.collect();
match-Vollständigkeit. Fügen Sie eine neue Variante zu einem Enum hinzu und jedes match, das sie nicht behandelt, wird zu einem Compile-Fehler. Nutzen Sie dies. Es ist eines der mächtigsten Refactoring-Werkzeuge in jeder Sprache.
Lassen Sie den Compiler Refactorings treiben. Der gleiche Trick funktioniert bei Strukturen: Fügen Sie ein Pflichtfeld ohne Default hinzu und jeder vorhandene Konstruktor wird zu einem Compile-Fehler – eine kostenlose, vollständige Arbeitsliste aller zu aktualisierenden Stellen.
if let und let else für den häufigen einarmigen Match.
if let Some(name) = user.name { println!("hallo {name}"); }
let Some(name) = user.name else { return Err(anyhow!("kein name")); };
// `name` ist von hier an im Gültigkeitsbereich, keine Verschachtelung
From / Into für Typkonvertierungen. Implementieren Sie From, erhalten Sie Into kostenlos. ? verwendet From, um Fehlertypen automatisch zu konvertieren.
Kombinatoren auf Option / Result. Greifen Sie zu .map, .and_then, .unwrap_or, .unwrap_or_else, .ok_or bevor Sie zu match greifen.
Ableitungsmakros. #[derive(Debug, Clone, PartialEq)] gibt Ihnen 80% des Boilerplates kostenlos. Fügen Sie #[derive(Serialize, Deserialize)] für JSON hinzu.
Neuer Zucker. Stabilisierte Funktionen, die es wert sind, bekannt zu sein, neueste zuerst: bool::ok_or und ok_or_else (1.98) verwandeln eine Bedingung direkt in ein Result (bereit.ok_or(Error::NichtBereit)?). cfg_select! ist ein Compile-Zeit-match über cfg-Prädikate und ersetzt die meisten Verwendungen der cfg-if-Crate, und if let-Guards funktionieren auf match-Armen (match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... }) – beide 1.95. assert_matches! und debug_assert_matches! (1.96) bestätigen ein Muster statt Gleichheit, was die natürliche Bestätigung für ein Enum ist. Zwei ältere, die Sie ständig sehen werden und nicht für exotisch halten sollten: Let-Chains (if let Some(x) = a && x > 3 { ... }), stabil in Edition 2024 seit 1.88, und async Closures (async || { ... }, mit der AsyncFn-Familie von Grenzen), stabil seit 1.85.
Von X kommend, hier ist, was Sie beißt
Von Python oder JavaScript:
let b = a;für einen Heap-Wert (wieString,Vec) verschiebt ihn.aist nicht mehr verwendbar. Verwenden Sie&azum Ausleihen odera.clone()zum Kopieren.- Kein null.
Option<T>wird Ihnen aufgezwungen. - Keine Ausnahmen.
Result<T, E>und?. Der Compiler lässt Sie Fehler nicht ignorieren. - Keine Vererbung. Komposition + Traits + Enums.
- Variablen sind standardmäßig unveränderlich. Fügen Sie
muthinzu, um zu ändern. Gleiches für Referenzen:&vs&mut. - Das erneute Deklarieren eines Namens ist erlaubt und idiomatisch:
let input = input.trim();überdeckt die alte Bindung mit einer neuen, möglicherweise eines anderen Typs. Es ist keine Änderung, und es ist, wie Rust-Code einen Wert an Ort und Stelle umwandelt, ohneinput2zu erfinden. - Ganzzahltypen sind explizit und die Indizierung erfordert
usize. Sie verhalten sich auch je nach Build unterschiedlich: Debug enthält "Prüfungen auf Ganzzahlüberlauf, die Ihr Programm zur Laufzeit panicken lassen", während--releasesie entfernt und eine "Zweierkomplement-Umwicklung" durchführt. Eine Testsuite, die besteht, kann trotzdem in der Produktion umwickeln, also verwenden Siechecked_*/saturating_*/wrapping_*, wo die Arithmetik tatsächlich die Grenze erreichen kann, anstatt sich auf den Debug-Panik zu verlassen, um es zu finden.strict_addund Freunde (stabil seit 1.91) panicken bei Überlauf in jedem Build, undoverflow-checks = trueunter[profile.release]behält das Debug-Verhalten in der Produktion bei. Undaszwischen Ganzzahltypen schneidet stillschweigend ab (300_u32 as u8ist44) – verwenden Sieu8::try_from(x)?, wo der Wert möglicherweise nicht passt. - Ein
Stringist nicht indizierbar.s[0]kompiliert nicht – siehereferences/ownership-and-types.mdfür das, was Sie stattdessen verwenden sollten.
Von Go:
- Fehler als Werte – gleicher Instinkt, aber verwenden Sie
?anstelle vonif err != nil. - Kein
nil.Option<T>. - Kein GC und keine Goroutinen: Ownership + Ausleihen, async/await mit
tokio. Das async-Modell ist kooperativ (awaitist ein expliziter Abgabepunkt), nicht präemptiv. interface{}wird zu Traits. Standardmäßig Generics für statischen Dispatch;Box<dyn Trait>nur, wenn Sie es brauchen.- Statisches Linken ist der Standard. Binärdateien sind größer, aber in sich geschlossen.
panic!sollte für nicht behebbare Bugs im Anwendungscode reserviert sein; verwenden Sie es nicht als Go-artiges "Log and Continue".
Von Java oder C#:
- Traits sind nicht standardmäßig Interfaces mit virtuellem Dispatch.
<T: Trait>ist monomorphisiert.dyn Traitist die opt-in dynamische Version. - Keine Null-Referenzen.
Option<T>. - Keine Ausnahmen.
Result<T, E>und?. - Keine Klassenvererbung. Verwenden Sie Enums für Summentypen, Traits für gemeinsames Verhalten.
- Kein GC: Ownership und Ausleihen bestimmen Lebensdauern.
Arc<T>ist das, was einer Java-Referenz am nächsten kommt. - Generics sind monomorphisiert, nicht typgelöscht.
Von C++:
- Wie RAII, aber der Borrow-Checker erzwingt es zur Compile-Zeit.
- Keine Kopier-/Verschiebekonstruktoren.
Cloneist explizit undCopyist ein Marker-Trait für billige bitweise Kopien. - Kein undefiniertes Verhalten in sicherem Code (theoretisch).
&ist eine zur Compile-Zeit geprüfte Ausleihe, kein roher Zeiger. Rohe Zeiger existieren (*const T,*mut T), erfordern aberunsafezum Dereferenzieren.- Smart Pointer sind
Box<T>(unique_ptr),Rc<T>(shared_ptr, einzelner Thread),Arc<T>(shared_ptr, threadsicher). - Makros sind hygienisch. Prozedurale Makros (derive, attribute, funktionsartig) sind, wie
serde,tokio::mainusw. funktionieren.
Die Crate-Kurzliste
Diese decken die meisten realen Anwendungen ab. Fügen Sie sie nach Bedarf hinzu; sie sind nicht alle erforderlich.
| Crate | Was es Ihnen bietet |
|---|---|
serde + serde_json | Serialisierung. #[derive(Serialize, Deserialize)] und Sie sind fertig |
tokio | Async-Runtime. #[tokio::main], tokio::spawn, async I/O |
anyhow | App-Fehlertyp. anyhow::Result<T>, bail!, context() |
thiserror | Bibliotheksfehler-Enums. #[derive(thiserror::Error)] |
clap | CLI-Argument-Parsing. #[derive(Parser)], automatische --help |
reqwest | HTTP-Client. Synchron oder async, TLS, Cookie-Unterstützung |
axum | Web-Framework. Basierend auf tower und hyper. Router, State, extractors |
sqlx | Datenbank. Asynchron, compile-geprüfte Abfragen (sqlx::query!), PostgreSQL/MySQL/SQLite/MSSQL |
tracing | Strukturiertes Logging. info!, error! mit Feldern, verteilte Ablaufverfolgung |
chrono | Datum/Uhrzeit. DateTime<Utc>, NaiveDate, Zeitzonen |
uuid | UUIDs. Uuid::new_v4(), Parsing, serde-Support |
regex | Reguläre Ausdrücke. Regex::new(r"^")?, zur Compile-Zeit mit once_cell oder LazyLock |
tempfile | Temporäre Dateien und Verzeichnisse. NamedTempFile, TempDir |
mockall | Mocking für Tests. #[automock], expect_* und returning |
rand | Zufallszahlen. thread_rng().gen_range(0..10), kryptografische Zufälligkeit |
Elf Crates + zwei Makro-Crates. Das ist die Wartungsfläche für 90% der Rust-Apps. Crates in Rust altern gut; sie in den Cargo.toml aufzunehmen ist günstig, da sie fast immer einfach funktionieren.
Minimale, aber meinungsstarke Cargo.toml
[package]
name = "my-app"
version = "0.1.0"
edition = "2024" # Edition 2021 oder 2024
[dependencies]
serde = { version = "1", features = ["derive"] }
anyhow = "1"
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
tempfile = "3"
[profile.release]
codegen-units = 1 # langsamer zu bauen, kleinerer/schnellerer Code
lto = "fat" # Link-Zeit-Optimierung, alle Crates
opt-level = 3 # maximale Optimierung auf Geschwindigkeit
overflow-checks = true # Überlauf fangen, auch in Release
[lints.rust]
unsafe_code = "forbid" # kein unsicherer Code erlaubt
[lints.clippy]
cargo = "warn" # warne, wenn Cargo-Manifest-Probleme vorliegen
pedantic = "warn" # scharfe, aber manchmal zu eifrige Prüfungen
[workspace.lints.clippy]
# Einige pedantische Lints sind übertrieben; ruhig ausschalten
cast_precision_loss = "allow"
module_name_repetitions = "allow"
.gitignore für ein Rust-Projekt
/target
**/*.rs.bk
.env
Nur das. /target ist der einzige Compiler-Artefakt-Ordner. *.rs.bk sind Backup-Dateien, die einige Editoren erstellen. .env für umgebungsabhängige Geheimnisse.
Ubuntu/Pakete, die Sie benötigen könnten
sudo apt update
sudo apt install build-essential pkg-config libssl-dev
build-essential (C-Compiler/Linker), pkg-config (von openssl benötigt), libssl-dev (für reqwest TLS). Je nach Ihren Crates: libpq-dev für PostgreSQL, libmysqlclient-dev für MySQL, libsqlite3-dev für SQLite, libfontconfig1-dev für iced,
libgtk-3-dev für gtk4-rs. Fehler wie failed to run custom build command for 'openssl-sys' bedeuten meist, dass ein -dev-Paket fehlt.
Verweise und Weiterführendes
- The Book – offiziell, umfangreich, und der Ausgangspunkt für eine tiefgehende Behandlung
- Rust by Example – lernt, indem Sie kommentierte Beispiele lesen
- std::docs – standard library –
Option,Result,Iterator– Sie werden diese Seite täglich aufrufen - Rustlings – interaktive Übungen, die den Borrow-Checker lehren
- This Week in Rust – wöchentliche Community-News
- crates.io – Crate-Registry
- docs.rs – Dokumentation für jedes veröffentlichte Crate