Files
2026-04-02 22:31:09 +02:00

14 KiB
Raw Permalink Blame History

---
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)

  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

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 &T oder
  • 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 &T
  • into_iter() konsumiert und gibt T (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.rs
  • src/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 eindeutig
  • Rc<T>: shared ownership single-thread
  • Arc<T>: shared ownership thread-safe
  • RefCell<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>
  • NoneOption<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


Vorschlag für deinen Lernpfad (kurz)

  1. Kapitel 17 (Syntax, Structs/Enums)
  2. Ownership/Borrowing (Kapitel 810) 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?