14 KiB
---
title: "Rust Tutorial (für Python-Erfahrene)"
tags: [rust, tutorial, programming]
created: 2026-03-27
---
# Rust Tutorial (für Python-Erfahrene)
Dieses Tutorial ist als **hands-on Lernpfad** gedacht: viele kleine Programme, die du direkt ausführen und variieren kannst. Du bekommst dabei die wichtigsten Rust-Konzepte (Ownership/Borrowing, Lifetimes-Grundlagen, Traits, Enums/Pattern Matching, Fehlerbehandlung, Collections, Generics, Module/Crates, Testing, Async-Basics).
---
## 0) Setup: Was brauche ich, wie führe ich Rust aus?
### Installation (Compiler + Tooling)
Rust wird normalerweise über **rustup** installiert (inkl. `rustc`, `cargo`, Standardbibliothek).
- **Windows/macOS/Linux:**
- https://rustup.rs öffnen und Anweisungen folgen
- Danach prüfen:
```bash
rustc --version
cargo --version
rustup --version
Editor (empfohlen)
- VS Code + Extension: rust-analyzer
- Alternativ: IntelliJ Rust, Neovim + rust-analyzer, etc.
Neues Projekt erstellen und ausführen
Rust nutzt Cargo als Build-Tool + Paketmanager.
cargo new hello_rust
cd hello_rust
cargo run
Wichtige Cargo-Kommandos:
cargo build # kompiliert (debug)
cargo build --release
cargo run # build + run
cargo test # tests
cargo fmt # formatieren (rustfmt)
cargo clippy # lints (clippy)
cargo doc --open # docs generieren + öffnen
Projektstruktur:
src/main.rs(Binary)Cargo.toml(Dependencies, Name, Version, etc.)
1) Hello World + Grundsyntax
src/main.rs:
fn main() {
println!("Hello, Rust!");
}
Wichtig: Makro-Aufruf erkennst du am ! (println!, vec!, format!, …).
2) Variablen, Mutabilität, Typen
Rust ist statisch typisiert, aber oft mit Typinferenz.
fn main() {
let x = 5; // immutable
// x = 6; // Fehler
let mut y = 5; // mutable
y = 6;
let z: i32 = 42; // expliziter Typ
let pi: f64 = 3.1415;
println!("{x} {y} {z} {pi}");
}
Shadowing (anders als mut)
fn main() {
let s = "42";
let s = s.parse::<i32>().unwrap(); // shadowing: neuer s mit anderem Typ
println!("{s}");
}
3) Strings: &str vs String (Python-Vergleich)
&str: String Slice, meist “geliehener” Text (z.B. Stringliteral)String: besitzender, heap-allocierter String (änderbar)
fn main() {
let a: &str = "hi"; // slice
let mut b: String = String::from("hi"); // owned
b.push_str(" there");
println!("{a} / {b}");
}
Sehr häufig konvertieren:
let s = "abc".to_string();
let t = String::from("abc");
let u: &str = &t; // String -> &str
4) Funktionen
fn add(a: i32, b: i32) -> i32 {
a + b // kein Semikolon => Ausdruck
}
fn main() {
let r = add(2, 3);
println!("{r}");
}
Mehr Rückgabewerte via Tupel:
fn div_mod(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
5) Kontrollfluss: if, loop, while, for
fn main() {
let n = 7;
if n % 2 == 0 {
println!("even");
} else {
println!("odd");
}
for i in 0..3 {
println!("i={i}");
}
let items = vec!["a", "b", "c"];
for (idx, item) in items.iter().enumerate() {
println!("{idx}: {item}");
}
}
loop kann Werte zurückgeben:
fn main() {
let mut i = 0;
let result = loop {
i += 1;
if i == 3 {
break i * 10; // liefert 30
}
};
println!("{result}");
}
6) Structs, Methoden, impl
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
}
impl User {
fn new(name: impl Into<String>, age: u32) -> Self {
Self { name: name.into(), age }
}
fn birthday(&mut self) {
self.age += 1;
}
}
fn main() {
let mut u = User::new("Alice", 30);
u.birthday();
println!("{u:?}");
}
7) Enums + Pattern Matching (sehr wichtig)
#[derive(Debug)]
enum Message {
Quit,
Write(String),
Move { x: i32, y: i32 },
}
fn handle(m: Message) {
match m {
Message::Quit => println!("bye"),
Message::Write(text) => println!("text={text}"),
Message::Move { x, y } => println!("move to {x},{y}"),
}
}
fn main() {
handle(Message::Write("hello".into()));
handle(Message::Move { x: 1, y: 2 });
}
match ist exhaustiv: alle Fälle müssen behandelt werden (oder _).
8) Ownership, Borrowing, References (Kern von Rust)
Rust verhindert Data Races und Use-After-Free durch Regeln:
Ownership-Regeln (vereinfacht)
- Jeder Wert hat genau einen Owner.
- Wenn der Owner aus dem Scope läuft, wird der Wert gedroppt.
- Move: Zuweisung/Übergabe kann Ownership übertragen.
Beispiel: Move vs Copy
fn main() {
let a = String::from("hello");
let b = a;
// println!("{a}"); // Fehler: a wurde gemoved
let x = 5; // i32 ist Copy
let y = x;
println!("{x} {y}");
}
Borrowing: &T (immutable) und &mut T (mutable)
fn len(s: &String) -> usize {
s.len()
}
fn add_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let mut s = String::from("hi");
println!("{}", len(&s));
add_exclamation(&mut s);
println!("{s}");
}
Borrowing-Regeln:
- beliebig viele
&Toder - genau ein
&mut T - aber nicht beides gleichzeitig (im gleichen Gültigkeitsbereich)
Typischer Fehler (und Fix durch Scopes):
fn main() {
let mut s = String::from("abc");
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}");
let r3 = &mut s; // ok, weil r1/r2 danach nicht mehr benutzt werden
r3.push('d');
println!("{r3}");
}
9) Slices: Teilansichten von Daten
fn main() {
let a = [10, 20, 30, 40];
let mid: &[i32] = &a[1..3]; // 20,30
println!("{mid:?}");
let s = String::from("hello world");
let w: &str = &s[0..5];
println!("{w}");
}
Achtung bei UTF-8: String-Slicing nur an gültigen Byte-Grenzen von Codepoints.
10) Collections: Vec, HashMap, HashSet
Vec<T>
fn main() {
let mut v = vec![1, 2, 3];
v.push(4);
for x in &v {
println!("{x}");
}
if let Some(last) = v.pop() {
println!("popped {last}");
}
}
HashMap<K,V>
use std::collections::HashMap;
fn main() {
let mut m = HashMap::new();
m.insert("alice", 10);
m.insert("bob", 7);
*m.entry("alice").or_insert(0) += 1;
if let Some(score) = m.get("alice") {
println!("alice={score}");
}
for (k, v) in &m {
println!("{k} => {v}");
}
}
11) Option und Result: Fehlerbehandlung ohne Exceptions
Rust nutzt Option<T> (statt None) und Result<T, E> (statt Exceptions).
Option
fn first(v: &[i32]) -> Option<i32> {
v.first().copied()
}
fn main() {
let v = vec![1, 2, 3];
match first(&v) {
Some(x) => println!("first={x}"),
None => println!("empty"),
}
}
Result + ? Operator
use std::fs;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let text = fs::read_to_string(path)?;
Ok(text)
}
fn main() {
match read_file("Cargo.toml") {
Ok(t) => println!("len={}", t.len()),
Err(e) => eprintln!("error: {e}"),
}
}
?: Wenn Err, returnt die Funktion früh; wenn Ok, entpackt.
12) Iterators (sehr “pythonic”, aber typisiert)
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let squares: Vec<i32> = nums.iter()
.map(|x| x * x)
.collect();
let even_sum: i32 = nums.iter()
.filter(|x| *x % 2 == 0)
.sum();
println!("{squares:?} even_sum={even_sum}");
}
Ownership dabei:
iter()gibt&Tinto_iter()konsumiert und gibtT(Owned)iter_mut()gibt&mut T
13) Generics + Traits (Rusts “Interfaces”)
Generics
fn first<T: Clone>(v: &[T]) -> Option<T> {
v.first().cloned()
}
Trait definieren und implementieren
trait Greeter {
fn greet(&self) -> String;
}
struct Person { name: String }
impl Greeter for Person {
fn greet(&self) -> String {
format!("Hi, {}!", self.name)
}
}
fn say_hello(g: &impl Greeter) {
println!("{}", g.greet());
}
fn main() {
let p = Person { name: "Alice".into() };
say_hello(&p);
}
Trait Bounds:
fn print_debug<T: std::fmt::Debug>(x: T) {
println!("{x:?}");
}
14) Module, Crates, Sichtbarkeit (pub)
In einer Datei
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn secret() {} // privat
}
fn main() {
println!("{}", math::add(1, 2));
}
Mehrere Dateien (typisches Layout)
src/main.rssrc/math.rs
src/main.rs:
mod math;
fn main() {
println!("{}", math::add(1, 2));
}
src/math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
15) Lifetimes (Grundidee, praxisnah)
Lifetimes sagen dem Compiler: “Wie lange sind Referenzen gültig?”
Beispiel: Funktion gibt eine Referenz zurück:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let x = "short";
let y = "a bit longer";
println!("{}", longer(x, y));
}
Als Anfänger-Regel:
- Wenn du Owned (
String,Vec<T>) zurückgeben kannst, ist das oft einfacher. - Lifetimes brauchst du vor allem bei APIs, die Referenzen zurückgeben.
16) Smart Pointers: Box, Rc, Arc, RefCell (Überblick)
Box<T>: heap allocation, Ownership bleibt eindeutigRc<T>: shared ownership single-threadArc<T>: shared ownership thread-safeRefCell<T>: “interior mutability” (Borrow-Regeln zur Laufzeit)
Mini-Beispiel Rc:
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);
println!("a={}, b={}", a, b);
println!("strong_count={}", Rc::strong_count(&a));
}
17) Concurrency (Basics): Threads + Channels
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
tx.send("hello from thread").unwrap();
});
let msg = rx.recv().unwrap();
println!("got: {msg}");
handle.join().unwrap();
}
18) Async (Basics): async/await mit Tokio
Async braucht i.d.R. eine Runtime (z.B. Tokio).
Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
src/main.rs:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let h = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
42
});
let result = h.await.unwrap();
println!("result={result}");
}
19) Testing
fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds() {
assert_eq!(add(2, 3), 5);
}
}
Ausführen:
cargo test
20) “Rust für Python-Dev”: mentale Übersetzungen
- Python-Liste ↔
Vec<T> - Dict ↔
HashMap<K,V> None↔Option<T>- Exceptions ↔
Result<T,E>+? - Duck typing ↔ Traits + Generics
- Mutable Default überall ↔ Rust: immutable by default
- Garbage Collector ↔ Ownership (Drop am Scope-Ende)
Idiom: Daten lieber so modellieren, dass match alle Fälle abdeckt (Enums sind stark).
21) Mini-Projekt 1: CLI “todo” (ohne externe Crates)
Ziel: einfache Aufgabenliste im Speicher (kein Persistenz).
use std::io::{self, Write};
fn main() {
let mut todos: Vec<String> = Vec::new();
loop {
print!("todo> ");
io::stdout().flush().unwrap();
let mut line = String::new();
if io::stdin().read_line(&mut line).is_err() {
println!("input error");
continue;
}
let line = line.trim();
if line == "quit" {
break;
} else if line == "list" {
for (i, t) in todos.iter().enumerate() {
println!("{}: {}", i + 1, t);
}
} else if let Some(rest) = line.strip_prefix("add ") {
todos.push(rest.to_string());
} else if let Some(rest) = line.strip_prefix("done ") {
if let Ok(idx) = rest.parse::<usize>() {
if idx >= 1 && idx <= todos.len() {
todos.remove(idx - 1);
} else {
println!("index out of range");
}
} else {
println!("usage: done <number>");
}
} else {
println!("commands: add <text> | list | done <n> | quit");
}
}
}
22) Mini-Projekt 2: JSON holen (Reqwest)
Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
src/main.rs:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct HttpBin {
url: String,
origin: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let data: HttpBin = reqwest::get("https://httpbin.org/get")
.await?
.json()
.await?;
println!("{data:#?}");
Ok(())
}
23) Nächste Schritte / gute Referenzen
- Offizielles Buch: https://doc.rust-lang.org/book/
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
- Standard Library Docs: https://doc.rust-lang.org/std/
Vorschlag für deinen Lernpfad (kurz)
- Kapitel 1–7 (Syntax, Structs/Enums)
- Ownership/Borrowing (Kapitel 8–10) – viel üben
- Option/Result + Iterators
- Traits/Generics + Module
- Tests + kleines Projekt
- Dann Async/Concurrency nach Bedarf
Offene Fragen (damit ich es besser anpassen kann)
- Willst du Rust eher für CLI, Backend, Embedded, WebAssembly oder Data/Perf nutzen?
- Betriebssystem/Editor?
- Soll ich daraus mehrere Obsidian-Notizen machen (z.B. “Ownership”, “Result/Option”, “Traits”, …) mit internen Links?