CommunityCodierung & Entwicklunggithub.com

tenequm/rust-dev

Leitfaden für den ersten Tag, um gut in Rust zu entwickeln – Ownership, Fehler als Werte, String vs &str, Box/Rc/Arc, anyhow vs thiserror und eine Kurzliste wichtiger Crates (tokio, serde, axum, sqlx). Verwenden Sie dies beim Start eines Rust-Projekts, im Kampf mit dem Borrow-Checker oder bei der Auswahl von Crates.

Was ist rust-dev?

Diese Rust-Development-Skill bietet eine praxisorientierte Einführung für den ersten Tag, die sich auf die wesentlichen Unterschiede zu anderen Sprachen und die grundlegenden Entscheidungen konzentriert, die den gesamten Entwicklungsprozess prägen. Sie richtet sich an Entwickler, die ein neues Rust-Projekt starten, aus anderen Sprachen wie Python, JavaScript, Go, Java/C# oder C++ kommen und mit Ownership, Ausleihen, Fehlerbehandlung und der Auswahl von Crates kämpfen. Die Skill behandelt fünf Kernkonzepte des Rust-Mentalmodells (Ownership, Aliasing XOR Mutability, Fehler als Werte, Traits, Borrow-Checker), eine Entscheidungstabelle für den ersten Tag, wichtige Redewendungen und eine Kurzliste essenzieller Crates. Sie ist für die Nutzung mit Claude Code optimiert, funktioniert aber auch mit anderen KI-Agenten, die Rust-Code generieren oder refaktorisieren.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/rust-dev

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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 (anyhow vs thiserror)
  • 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 an b. a ist weg.
  • Es unveränderlich ausleihen: &a erlaubt anderen, es anzusehen. Viele Leser erlaubt.
  • Es veränderlich ausleihen: &mut a erlaubt 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 von f für jeden konkreten T, mit dem Sie es aufrufen (Monomorphisierung, wie C++-Templates). Kein Laufzeit-Overhead.
  • Dynamischer Dispatch ist opt-in über dyn Trait (typischerweise Box<dyn Trait> oder &dyn Trait). Eine Vtable-Lookup pro Aufruf.
  • Keine Vererbung. Traits komponieren. Wenn Sie nach Deref greifen, um einen Typ zu "erweitern", hören Sie auf und verwenden Sie Komposition oder ein Enum.
  • Waisenregel: Sie können YourTrait for SomeoneElsesType oder SomeoneElsesTrait for YourType implementieren, 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 &str gegenüber String, &[T] gegenüber Vec<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 &str in 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.

EntscheidungStandardWann das andere wählen
String vs &str (Strukturfeld)StringFast nie &str, bis Sie einen echten Grund haben und Lifetimes verstehen
String vs &str (Funktionsparam)&strVerwenden 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 DispatchBox<dyn Trait>, wenn Sie eine heterogene Sammlung benötigen (Vec<Box<dyn Animal>>)
Fehler in Anwendungscodeanyhow::Result<T> überall-
Fehler in Bibliothekscodethiserror-abgeleitetes EnumNiemals 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-
ModulanordnungInline-Module, bis eine Datei lang wird, dann aufteilenEin 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 (wie String, Vec) verschiebt ihn. a ist nicht mehr verwendbar. Verwenden Sie &a zum Ausleihen oder a.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 mut hinzu, 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, ohne input2 zu 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 --release sie entfernt und eine "Zweierkomplement-Umwicklung" durchführt. Eine Testsuite, die besteht, kann trotzdem in der Produktion umwickeln, also verwenden Sie checked_*/saturating_*/wrapping_*, wo die Arithmetik tatsächlich die Grenze erreichen kann, anstatt sich auf den Debug-Panik zu verlassen, um es zu finden. strict_add und Freunde (stabil seit 1.91) panicken bei Überlauf in jedem Build, und overflow-checks = true unter [profile.release] behält das Debug-Verhalten in der Produktion bei. Und as zwischen Ganzzahltypen schneidet stillschweigend ab (300_u32 as u8 ist 44) – verwenden Sie u8::try_from(x)?, wo der Wert möglicherweise nicht passt.
  • Ein String ist nicht indizierbar. s[0] kompiliert nicht – siehe references/ownership-and-types.md für das, was Sie stattdessen verwenden sollten.

Von Go:

  • Fehler als Werte – gleicher Instinkt, aber verwenden Sie ? anstelle von if err != nil.
  • Kein nil. Option<T>.
  • Kein GC und keine Goroutinen: Ownership + Ausleihen, async/await mit tokio. Das async-Modell ist kooperativ (await ist 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 Trait ist 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. Clone ist explizit und Copy ist 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 aber unsafe zum 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::main usw. funktionieren.

Die Crate-Kurzliste

Diese decken die meisten realen Anwendungen ab. Fügen Sie sie nach Bedarf hinzu; sie sind nicht alle erforderlich.

CrateWas es Ihnen bietet
serde + serde_jsonSerialisierung. #[derive(Serialize, Deserialize)] und Sie sind fertig
tokioAsync-Runtime. #[tokio::main], tokio::spawn, async I/O
anyhowApp-Fehlertyp. anyhow::Result<T>, bail!, context()
thiserrorBibliotheksfehler-Enums. #[derive(thiserror::Error)]
clapCLI-Argument-Parsing. #[derive(Parser)], automatische --help
reqwestHTTP-Client. Synchron oder async, TLS, Cookie-Unterstützung
axumWeb-Framework. Basierend auf tower und hyper. Router, State, extractors
sqlxDatenbank. Asynchron, compile-geprüfte Abfragen (sqlx::query!), PostgreSQL/MySQL/SQLite/MSSQL
tracingStrukturiertes Logging. info!, error! mit Feldern, verteilte Ablaufverfolgung
chronoDatum/Uhrzeit. DateTime<Utc>, NaiveDate, Zeitzonen
uuidUUIDs. Uuid::new_v4(), Parsing, serde-Support
regexReguläre Ausdrücke. Regex::new(r"^")?, zur Compile-Zeit mit once_cell oder LazyLock
tempfileTemporäre Dateien und Verzeichnisse. NamedTempFile, TempDir
mockallMocking für Tests. #[automock], expect_* und returning
randZufallszahlen. 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

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Verwandte Skills