746 lines
14 KiB
Markdown
746 lines
14 KiB
Markdown
```md
|
||
---
|
||
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.
|
||
|
||
```bash
|
||
cargo new hello_rust
|
||
cd hello_rust
|
||
cargo run
|
||
```
|
||
|
||
Wichtige Cargo-Kommandos:
|
||
```bash
|
||
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`:
|
||
```rust
|
||
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**.
|
||
|
||
```rust
|
||
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)
|
||
```rust
|
||
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)
|
||
|
||
```rust
|
||
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:
|
||
```rust
|
||
let s = "abc".to_string();
|
||
let t = String::from("abc");
|
||
let u: &str = &t; // String -> &str
|
||
```
|
||
|
||
---
|
||
|
||
## 4) Funktionen
|
||
|
||
```rust
|
||
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:
|
||
```rust
|
||
fn div_mod(a: i32, b: i32) -> (i32, i32) {
|
||
(a / b, a % b)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5) Kontrollfluss: `if`, `loop`, `while`, `for`
|
||
|
||
```rust
|
||
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:
|
||
```rust
|
||
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`
|
||
|
||
```rust
|
||
#[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)
|
||
|
||
```rust
|
||
#[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)
|
||
1. Jeder Wert hat **genau einen Owner**.
|
||
2. Wenn der Owner aus dem Scope läuft, wird der Wert **gedroppt**.
|
||
3. **Move**: Zuweisung/Übergabe kann Ownership übertragen.
|
||
|
||
Beispiel: Move vs Copy
|
||
```rust
|
||
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)
|
||
```rust
|
||
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 `&T` **oder**
|
||
- genau ein `&mut T`
|
||
- aber nicht beides gleichzeitig (im gleichen Gültigkeitsbereich)
|
||
|
||
Typischer Fehler (und Fix durch Scopes):
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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>`
|
||
```rust
|
||
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>`
|
||
```rust
|
||
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
|
||
```rust
|
||
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
|
||
```rust
|
||
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)
|
||
|
||
```rust
|
||
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 `&T`
|
||
- `into_iter()` konsumiert und gibt `T` (Owned)
|
||
- `iter_mut()` gibt `&mut T`
|
||
|
||
---
|
||
|
||
## 13) Generics + Traits (Rusts “Interfaces”)
|
||
|
||
### Generics
|
||
```rust
|
||
fn first<T: Clone>(v: &[T]) -> Option<T> {
|
||
v.first().cloned()
|
||
}
|
||
```
|
||
|
||
### Trait definieren und implementieren
|
||
```rust
|
||
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:
|
||
```rust
|
||
fn print_debug<T: std::fmt::Debug>(x: T) {
|
||
println!("{x:?}");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 14) Module, Crates, Sichtbarkeit (`pub`)
|
||
|
||
### In einer Datei
|
||
```rust
|
||
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.rs`
|
||
- `src/math.rs`
|
||
|
||
`src/main.rs`:
|
||
```rust
|
||
mod math;
|
||
|
||
fn main() {
|
||
println!("{}", math::add(1, 2));
|
||
}
|
||
```
|
||
|
||
`src/math.rs`:
|
||
```rust
|
||
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:
|
||
```rust
|
||
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 eindeutig
|
||
- `Rc<T>`: shared ownership **single-thread**
|
||
- `Arc<T>`: shared ownership **thread-safe**
|
||
- `RefCell<T>`: “interior mutability” (Borrow-Regeln zur Laufzeit)
|
||
|
||
Mini-Beispiel `Rc`:
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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`:
|
||
```toml
|
||
[dependencies]
|
||
tokio = { version = "1", features = ["full"] }
|
||
```
|
||
|
||
`src/main.rs`:
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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:
|
||
```bash
|
||
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).
|
||
|
||
```rust
|
||
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`:
|
||
```toml
|
||
[dependencies]
|
||
tokio = { version = "1", features = ["full"] }
|
||
reqwest = { version = "0.12", features = ["json"] }
|
||
serde = { version = "1", features = ["derive"] }
|
||
```
|
||
|
||
`src/main.rs`:
|
||
```rust
|
||
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)
|
||
1. Kapitel 1–7 (Syntax, Structs/Enums)
|
||
2. Ownership/Borrowing (Kapitel 8–10) – viel üben
|
||
3. Option/Result + Iterators
|
||
4. Traits/Generics + Module
|
||
5. Tests + kleines Projekt
|
||
6. 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?
|
||
``` |