Skip to content

Rust Spickzettel

Systemsprache, fokussiert auf Sicherheit, Geschwindigkeit und Nebenläufigkeit.

01

Grundlagen

Variablen & Veränderlichkeit

Rust-Variablen sind standardmäßig unveränderlich – das ist ein Kern-Sicherheits-Feature, das versehentliche Mutation verhindert. Verwende 'mut' nur, wenn du einen Wert wirklich ändern musst. 'const' erfordert einen expliziten Typ und wird zur Compile-Zeit inlined, während 'static' eine feste Speicheradresse mit einer 'static-Lifetime hat.

rust
let x = 5;            // immutable by default
let mut y = 10;        // mutable with 'mut'
y += 1;                // OK, y is mutable

const MAX: u32 = 100;  // compile-time constant
static GREETING: &str = "Hi"; // global static

let z: i32 = -5;       // explicit type annotation

Shadowing

Shadowing lässt dich einen Variablennamen wiederverwenden, während du seinen Typ oder Wert änderst. Anders als 'mut' erstellt Shadowing eine neue Bindung – nützlich für das Transformieren von Daten (z.B. einen String zu int parsen), ohne einen neuen Namen zu erfinden. Der alte Wert wird nach der neuen Bindung gedroppt.

rust
let x = 5;
let x = x * 2;         // shadow previous x, now 10
let x = "text";        // can even change type!
println!("{}", x);     // "text"

// shadowing vs mut: shadowing creates a NEW variable
// with the same name, allowing type changes

Kommentare & Drucken

Verwende /// für Item-Dokumentation (von 'cargo doc' angezeigt) und //! für Modul-/Crate-Docs. println! schreibt nach stdout, eprintln! nach stderr. Der {}-Platzhalter unterstützt Formatierungs-Specs: > für Rechtsbündig, < für Linksbündig, ^ für Zentriert, .N für Präzision, #b/#o/#x für Binär/Oktal/Hex.

rust
// Line comment
/* Block comment */
/// Doc comment (renders in rustdoc)
//! Module-level doc comment

let name = "Alice";
println!("Hello, {}!", name);        // format string
println!("{0} {1} {0}", "a", "b");   // positional args
println!("{:>5}", 42);               // right-align, width 5
println!("{:.2}", 3.14159);          // 2 decimal places: 3.14
println!("{:#b}", 0b1010);           // binary: 0b1010
eprintln!("Error to stderr");        // error output

Datentypen-Überblick

Rust hat keine implizite Typkonvertierung – verwende 'as' für Casts. i32 ist der Standard-Integer-Typ, f64 der Standard-Float. usize wird für Indizierung und Größen verwendet. char ist ein voller Unicode-Skalarwert (kein Byte), also ist '🦀' ein einzelnes char.

rust
// Scalar types
let a: i32 = 42;          // signed 32-bit integer
let b: u64 = 100;         // unsigned 64-bit
let c: f64 = 3.14;        // 64-bit float (default)
let d: bool = true;
let e: char = '🦀';        // 4-byte Unicode scalar

// isize/usize: pointer-sized (32 or 64 bit depending on arch)
let len: usize = vec![1,2,3].len();

// Compound types
let pair: (i32, &str) = (1, "hello");
let arr: [i32; 3] = [0; 3];   // [0, 0, 0]

Type-Casting & Aliase

Der 'as'-Cast ist ungeprüft und kann Daten verlieren (z.B. 300u16 as u8 wrappt zu 44). Für sichere Konvertierungen verwende From/Into-Traits, die für verlustfreie Casts implementiert sind. Type-Aliase verbessern die Lesbarkeit, ohne neue Typen zu erstellen – verwende 'struct NewType(i32)' für ein distinct Newtype-Pattern.

rust
// 'as' keyword for primitive casts (may truncate)
let x: i32 = 42;
let y: f64 = x as f64;        // 42.0
let z: u8 = 300u16 as u8;     // truncated: 44

// From/Into traits for safe conversion
let s = String::from("hi");
let owned: String = "hi".into();

// Type aliases
type Kilometers = i32;
let distance: Kilometers = 5;
02

Strings

String vs &str

Die Unterscheidung zwischen String (owned, Heap) und &str (geliehener Slice) ist fundamental in Rust. Verwende String, wenn du den Text besitzen/modifizieren/wachsen musst; verwende &str, wenn du ihn nur lesen musst. &str kann entweder auf den Buffer eines Strings oder ein String-Literal im Binary zeigen. Bevorzuge &str in Funktionsparametern für Flexibilität.

rust
// &str: immutable string slice (borrowed)
let s1: &str = "literal";           // stored in binary
let s2: &str = &s3[..2];            // slice of a String

// String: owned, growable, heap-allocated
let mut s3: String = String::from("hello");
s3.push_str(", world");             // append
s3.push('!');                       // append char
s3.insert(0, '>');                  // insert at index
println!("{}", s3);                 // >hello, world!

// Convert: &str -> String
let owned = "literal".to_string();
let owned2 = String::from("literal");

String-Methoden

String-Methoden geben neue owned Strings zurück, anstatt in-place zu modifizieren (außer push_*/insert-Methoden auf veränderlichen Strings). split() gibt einen Iterator zurück, also verwende .collect() zum Materialisieren. Beachte, dass Indizierung s[0] auf Strings NICHT erlaubt ist, weil UTF-8-Grenzen nicht mit Byte-Indizes übereinstimmen – verwende stattdessen s.chars().nth(0).

rust
let s = String::from("Hello, World");

// inspection
println!("len: {}", s.len());              // 12
println!("is_empty: {}", s.is_empty());    // false
println!("contains 'World': {}", s.contains("World")); // true
println!("starts_with 'Hello': {}", s.starts_with("Hello")); // true

// transformation
let upper = s.to_uppercase();              // HELLO, WORLD
let replaced = s.replace("o", "0");        // Hell0, W0rld
let trimmed = "  hi  ".trim().to_string(); // hi

// splitting
for word in s.split(", ") {
    println!("{}", word); // Hello / World
}
let parts: Vec<&str> = s.split_whitespace().collect();

Formatierung & Konkatenation

Der +-Operator übernimmt das Ownership des linken Strings und leiht den rechten (&str). Deshalb wird s1 nach s1 + &s2 ungültig. Für das Verketten mehrerer Strings bevorzuge format!, das lesbarer ist und keine Operanden bewegt. concat! funktioniert nur mit Literalen und produziert ein &'static str.

rust
// format! macro creates a new String
let name = "Alice";
let msg = format!("Hi {}, you have {} messages", name, 5);

// concatenation
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2;        // s1 moved, s2 borrowed
// s1 is now invalid!

let s4 = format!("{}{}", s2, s3); // neither moved

// concat! macro for literals
let s5 = concat!("foo", "bar"); // "foobar" at compile time

Zeichen & Bytes iterieren

Rust-Strings sind UTF-8-kodiert, also Byte-Index != char-Index. chars() iteriert Unicode-Skalarwerte (O(n) zum Dekodieren), bytes() iteriert rohe Bytes. Slicing mit [n..m] panikt, wenn n oder m in die Mitte eines Multi-Byte-Zeichens fallen. Für Byte-Level-Zugriff konvertiere zu Vec<u8> via as_bytes().

rust
let s = "héllo";  // é is 2 bytes in UTF-8

// iterate over chars (Unicode scalar values)
for c in s.chars() {
    print!("{} ", c);  // h é l l o
}

// iterate over bytes
for b in s.bytes() {
    print!("{} ", b);  // 104 195 169 108 108 111
}

// get char at index (O(n) — must walk UTF-8)
let third = s.chars().nth(2); // Some('l')

// string slices must be on char boundaries
let slice = &s[0..1]; // "h" — OK
// let bad = &s[0..2]; // PANIC if mid-é!

Parsen & Konvertierung

parse() gibt Result zurück, weil der String möglicherweise keine gültige Zahl ist – behandle immer den Error. Die Turbofish-Syntax parse::<T>() lässt dich den Typ inline angeben. Das Konvertieren von String zu &str ist kostenlos (nur ein Borrow), aber &str zu String allokiiert. collect() kann einen String aus einem Iterator von chars bauen.

rust
// String/str -> number
let n: i32 = "42".parse().unwrap();
let n2 = "42".parse::<i32>().unwrap(); // turbofish syntax
let f: f64 = "3.14".parse().unwrap();

// number -> String
let s = 42.to_string();
let s2 = format!("{}", 42);

// String -> &str (free, just dereference)
let owned = String::from("hi");
let borrowed: &str = &owned;

// collect chars into String
let upper: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
03

Datenstrukturen

Arrays & Slices

Arrays [T; N] haben eine zur Compile-Zeit bekannte feste Größe und leben auf dem Stack. Slices &[T] sind Fat-Pointer (Pointer + Länge), die eine zusammenhängende Sequenz leihen – sie erlauben Funktionen, jedes Array oder Vector ohne Größenbedenken zu akzeptieren. Verwende Slices in Funktionssignaturen für Allgemeinheit.

rust
// Fixed-size array (stack allocated)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5];           // [0, 0, 0, 0, 0]
println!("first: {}", arr[0]);
println!("len: {}", arr.len());

// Slice: a view into an array/vector
let slice: &[i32] = &arr[1..3];  // [2, 3]
let full: &[i32] = &arr;          // whole array
let first = &arr[..1];            // [1]

// iterating
for n in &arr {
    println!("{}", n);
}

Vectors (Vec<T>)

Vec<T> ist Rusts wachsendes Array, unterlegt von Heap-Speicher mit Kapazitätsverdopplung. push/pop sind O(1) amortisiert; insert/remove sind O(n), weil Elemente verschoben werden. Verwende .get(i) statt v[i], wenn du sicheren Zugriff willst (gibt Option zurück). into_iter() konsumiert den Vector und yielded owned Werte.

rust
// growable heap array
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3];          // macro shorthand

v.push(4);                        // append
v.pop();                          // remove last -> Option
v.insert(0, 0);                   // insert at index (O(n))
v.remove(0);                      // remove at index (O(n))
v.extend([5, 6]);                 // append multiple

// access
println!("{}", v[0]);             // panics if out of bounds
println!("{:?}", v.get(0));       // Some(&4) — safe

// iterate by value, ref, or mut ref
for n in &v { print!("{}", n); }
for n in &mut v { *n *= 2; }      // double each
let owned: Vec<i32> = v.into_iter().collect();

HashMap & BTreeMap

HashMap verwendet Hashing für O(1)-Durchschnittszugriff, hat aber keine Sortierung. BTreeMap verwendet einen B-Baum für O(log n)-Zugriff, behält aber Schlüssel sortiert. Verwende HashMap, wenn du schnelle Lookups brauchst; verwende BTreeMap, wenn du sortierte Iteration oder Bereichs-Queries brauchst. entry().or_insert() ist der idiomatische Weg zum 'Upsert' – es gibt eine veränderliche Referenz auf den Wert zurück und fügt einen Default ein, falls abwesend.

rust
use std::collections::HashMap;
use std::collections::BTreeMap;

// HashMap: O(1) average lookup, unordered
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry("Bob".into()).or_insert(0); // insert if absent
let alice = scores.get("Alice"); // Some(&10)

// iterate (unordered)
for (name, score) in &scores {
    println!("{}: {}", name, score);
}

// BTreeMap: O(log n) lookup, sorted by key
let mut bt: BTreeMap<String, i32> = BTreeMap::new();
bt.insert("zebra".into(), 1);
bt.insert("apple".into(), 2);
// iterates in sorted order: apple, zebra

HashSet & BTreeSet

HashSet speichert eindeutige Werte mit O(1)-Mitgliedschaftsprüfungen. Set-Operationen (Union, Intersection, Differenz, Symmetrische Differenz) geben Iteratoren zurück. Verwende Sets für Deduplizierung, Mitgliedschaftsprüfung und mathematische Set-Operationen. BTreeSet ist das sortierte Äquivalent, unterlegt von BTreeMap.

rust
use std::collections::HashSet;

let mut a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5].into_iter().collect();

a.insert(4);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));

// set operations
let union: HashSet<_> = a.union(&b).copied().collect();
let inter: HashSet<_> = a.intersection(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();
let sym: HashSet<_> = a.symmetric_difference(&b).copied().collect();

Tuples & Destructuring

Tuples gruppieren eine feste Anzahl von Werten potenziell unterschiedlicher Typen. Greife auf Felder mit .0, .1 usw. zu. Destructuring mit let-Patterns ist idiomatisch. Der Unit-Typ () hat einen Wert () und wird wie void in anderen Sprachen verwendet. Tuples werden häufig verwendet, um mehrere Werte aus Funktionen zurückzugeben.

rust
// tuples can hold different types
let tup: (i32, f64, &str) = (42, 3.14, "hi");

// access by index
println!("{} {} {}", tup.0, tup.1, tup.2);

// destructuring
let (n, pi, s) = tup;
println!("{} {} {}", n, pi, s);

// nested
let ((a, b), c) = ((1, 2), 3);

// unit tuple (empty)
let unit: () = ();

// function returning multiple values
fn divmod(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}
let (q, r) = divmod(17, 5); // (3, 2)
04

Kontrollfluss

If / Else If / Else

Anders als in vielen Sprachen ist 'if' in Rust ein Ausdruck, der einen Wert zurückgibt. Das eliminiert die Notwendigkeit eines ternären Operators (Bedingung ? a : b) – verwende einfach if/else. Beide Zweige müssen denselben Typ zurückgeben. Die Bedingung braucht KEINE Klammern, aber der Body muss ein Block { } sein.

rust
let score = 85;

// if is an expression — returns a value
let grade = if score >= 90 {
    "A"
} else if score >= 80 {
    "B"
} else if score >= 70 {
    "C"
} else {
    "F"
};
println!("Grade: {}", grade);

// arms must return same type
// let bad = if true { 1 } else { "two" }; // ERROR!

Schleifen: loop, while, for

loop erstellt eine Endlosschleife – break verlässt sie und kann einen Wert zurückgeben (break value). while prüft eine Bedingung vor jeder Iteration. for ist die häufigste Schleife, iteriert über Ranges, Arrays, Vektoren, Iteratoren. Verwende 'continue', um zur nächsten Iteration zu springen. Ranges: a..b ist exklusiv, a..=b ist inklusiv.

rust
// loop: infinite, use break to exit
let mut count = 0;
let result = loop {
    count += 1;
    if count == 10 {
        break count * 2; // break with value
    }
};
println!("{}", result); // 20

// while: condition-checked loop
let mut n = 5;
while n > 0 {
    println!("{}", n);
    n -= 1;
}

// for: iterate over anything with IntoIterator
for i in 0..5 { print!("{}", i); }      // 01234
for i in (1..=3).rev() { print!("{}", i); } // 321
for ch in "hello".chars() { print!("{}", ch); }

Match (Pattern-Matching)

match ist Rusts mächtiges Pattern-Matching-Konstrukt. Es muss erschöpfend sein (alle Möglichkeiten abgedeckt) – verwende _ als Catch-All. Patterns unterstützen Literale, Ranges (..=), Or-Patterns (|), Bindings und Guards (if). match ist ein Ausdruck und gibt einen Wert zurück. Es ist der idiomatische Weg, Enums wie Option und Result zu behandeln.

rust
let coin = 25;
match coin {
    25 => println!("quarter"),
    10 => println!("dime"),
    5 => println!("nickel"),
    1 => println!("penny"),
    _ => println!("unknown"),  // catch-all (required)
}

// matching with bindings
let x = 3;
match x {
    1 | 2 => println!("one or two"),   // or-pattern
    3..=9 => println!("single digit"),  // range
    n if n % 2 == 0 => println!("even: {}", n), // guard
    _ => println!("other"),
}

// match is exhaustive — all cases must be covered

If Let & While Let

if let ist syntaktischer Zucker für match, wenn du nur um eine Variante kümmerst. Es ist weniger ausführlich, aber weniger erschöpfend als match – verwende es, wenn die anderen Cases nicht wichtig sind. while let schleift, solange das Pattern matcht, häufig mit Iteratoren verwendet (next() gibt Option zurück). Der else-Zweig ist optional.

rust
// if let: shorthand for matching one pattern
let maybe: Option<i32> = Some(5);

// verbose: match
match maybe {
    Some(x) => println!("got {}", x),
    None => println!("nothing"),
}

// concise: if let
if let Some(x) = maybe {
    println!("got {}", x);
} else {
    println!("nothing");
}

// while let: loop while pattern matches
let mut iter = vec![1, 2, 3].into_iter();
while let Some(n) = iter.next() {
    println!("{}", n);
}

Break, Continue & Labels

Labels (einfaches Anführungszeichen + Name) erlauben das Brechen oder Fortsetzen äußerer Schleifen aus verschachtelten Schleifen. Essenziell, wenn du mehrere Schleifen-Ebenen gleichzeitig verlassen musst. Ohne Labels beeinflussen break/continue nur die innerste Schleife. Labels sind eine saubere Alternative zu Flag-Variablen.

rust
// continue: skip to next iteration
for i in 0..10 {
    if i % 2 == 0 { continue; }
    println!("{}", i); // prints odd numbers
}

// break: exit loop
for i in 0..10 {
    if i == 5 { break; }
    println!("{}", i); // prints 0..4
}

// labeled loops (for nested break/continue)
'outer: for i in 0..3 {
    for j in 0..3 {
        if i == 1 && j == 1 {
            break 'outer; // breaks the outer loop
        }
        println!("{} {}", i, j);
    }
}
05

Funktionen & Closures

Funktionen definieren

Funktionen verwenden das 'fn'-Schlüsselwort. Der letzte Ausdruck ohne Semikolon ist der Rückgabewert (Expression). Ein Semikolon macht es zu einem Statement, das () zurückgibt. Verwende explizites 'return' nur für frühe Ausstiege. Der !-Rückgabetyp markiert divergierende Funktionen, die nie zurückgeben (Endlosschleifen, Panics, Prozess-Exits).

rust
// basic function with return type
fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon = expression = return value
}

// statements (with semicolon) return ()
fn greet(name: &str) {
    println!("Hi, {}", name);
    // implicit return ()
}

// explicit return
fn abs(x: i32) -> i32 {
    if x < 0 {
        return -x;  // early return needs 'return'
    }
    x  // tail expression
}

// diverging function (never returns)
fn forever() -> ! {
    loop {}
}

Parameter & Argumente

Rust hat kein Function-Overloading oder optionale Parameter (verwende Generics oder Builder stattdessen). Wähle Parametertypen sorgfältig: &T für Lesezugriff, &mut T für Schreibzugriff, T für Ownership-Transfer. Slices (&[T]) sind der idiomatische Weg, um variabel-lange Sequenzen zu akzeptieren. Default-Argumente werden nicht unterstützt – verwende das Builder-Pattern oder Option<T>.

rust
// immutable borrow
fn len(s: &String) -> usize { s.len() }

// mutable borrow
fn push(v: &mut Vec<i32>) { v.push(42); }

// take ownership
fn consume(s: String) { println!("{}", s); }

// multiple return via tuple
fn swap(a: i32, b: i32) -> (i32, i32) { (b, a) }

// variadic-ish via slices
fn sum(nums: &[i32]) -> i32 {
    nums.iter().sum()
}
println!("{}", sum(&[1, 2, 3, 4])); // 10

Closures

Closures sind anonyme Funktionen, die ihre Umgebung erfassen können. Sie werden nach Verwendung abgeleitet. Closures erfassen standardmäßig per Referenz; 'move' erzwingt Ownership-Transfer (essenziell für Threads). Closures implementieren Fn (Borrow), FnMut (Mut Borrow) oder FnOnce (Consume)-Traits, was sie als Funktionsparameter übergabefähig macht.

rust
// closure syntax: |params| body
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 3

// type annotations (rarely needed)
let square = |x: i32| -> i32 { x * x };

// capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15

// move closure: takes ownership of captured vars
let name = String::from("Alice");
let greet = move || println!("Hi {}", name);
// name is now moved into greet
greet();

Higher-Order-Funktionen & Iteratoren

Rust-Iteratoren sind lazy – Operationen führen nicht aus, bis .collect() oder eine andere konsumierende Methode aufgerufen wird. Das ermöglicht Zero-Cost-Abstraktion: Der Compiler kann verkettete Iterator-Methoden zu effizienten Schleifen optimieren. Häufige Methoden: map (transformieren), filter (auswählen), fold (akkumulieren), take (begrenzen), skip, enumerate, zip, flat_map.

rust
let nums = vec![1, 2, 3, 4, 5];

// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();

// filter: keep elements matching predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// fold/reduce: accumulate
let sum: i32 = nums.iter().sum();              // 15
let product: i32 = nums.iter().product();       // 120
let combined = nums.iter().fold(0, |acc, x| acc + x);

// chain multiple operations (lazy!)
let result: Vec<i32> = nums.iter()
    .filter(|&&x| x > 1)
    .map(|&x| x * x)
    .collect(); // [4, 9, 16, 25]

Funktions-Pointer & Traits

fn (kleingeschrieben) ist ein Funktions-Pointer-Typ – Zero-Cost, kann aber die Umgebung nicht erfassen. Für Closures, die erfassen, verwende generische <F: Fn(...)>-Bounds. Fn leiht, FnMut leiht veränderlich, FnOnce konsumiert. Funktions-Pointer sind nützlich, um Funktionen in Structs zu speichern oder an C zu übergeben. Generics mit Fn-Bounds sind flexibler und immer noch Zero-Cost, wenn monomorphisiert.

rust
// function pointer type
type MathFn = fn(i32, i32) -> i32;

fn add(a: i32, b: i32) -> i32 { a + b }
fn mul(a: i32, b: i32) -> i32 { a * b }

fn apply(f: MathFn, a: i32, b: i32) -> i32 {
    f(a, b)
}
println!("{}", apply(add, 3, 4)); // 7
println!("{}", apply(mul, 3, 4)); // 12

// generic over Fn trait (accepts closures too)
fn apply_fn<F: Fn(i32, i32) -> i32>(f: F, a: i32, b: i32) -> i32 {
    f(a, b)
}
let closure = |a, b| a - b;
println!("{}", apply_fn(closure, 10, 3)); // 7
06

Ownership & Borrowing

Ownership-Regeln

Ownership ist Rusts Kern-Speicherverwaltungssystem – kein Garbage Collector nötig. Wenn du einen Heap-Wert (String, Vec) zuweist, BEWEGT sich Ownership und die alte Variable wird ungültig. Stack-Typen (i32, f64, bool, char, Tuples von Copy-Typen) implementieren Copy und werden dupliziert statt bewegt. Das eliminiert Use-After-Free- und Double-Free-Bugs zur Compile-Zeit.

rust
// Rule 1: Each value has ONE owner
let s1 = String::from("hello");
let s2 = s1;  // s1's ownership MOVED to s2
// println!("{}", s1); // ERROR: s1 is invalid after move

// Rule 2: When owner goes out of scope, value is dropped
{
    let s = String::from("temp");
    // s is valid here
} // s is automatically dropped (memory freed)

// Rule 3: Copy types (i32, bool, char, etc.) are copied, not moved
let a = 5;
let b = a;  // a is copied, both valid
println!("{} {}", a, b); // OK

Borrowing & Referenzen

Borrowing lässt dich einen Wert verwenden, ohne Ownership zu übernehmen. &T erstellt eine unveränderliche Referenz – du kannst viele gleichzeitig haben. &mut T erstellt eine veränderliche Referenz – aber nur EINE veränderliche Referenz ODER beliebig viele unveränderliche Referenzen, nie beides. Das verhindert Data Races zur Compile-Zeit. Referenzen müssen immer auf gültige Daten zeigen (keine Dangling-Pointer).

rust
// &T: immutable borrow (read-only, multiple allowed)
fn calc_len(s: &String) -> usize {
    s.len()
    // s goes out of scope but is NOT dropped (we don't own it)
}
let s = String::from("hello");
let len = calc_len(&s);  // borrow s, don't move it
println!("'{}' has length {}", s, len); // s still valid

// &mut T: mutable borrow (exclusive, only ONE at a time)
fn push_world(s: &mut String) {
    s.push_str(", world");
}
let mut s2 = String::from("hello");
push_world(&mut s2);
println!("{}", s2); // hello, world

Slice-Referenzen

Slices sind Referenzen auf einen zusammenhängenden Teil einer Collection. Sie sind 'Fat-Pointer', die einen Pointer und eine Länge enthalten. String-Slices (&str) lassen Funktionen sowohl String als auch String-Literale akzeptieren. Array-Slices (&[T]) funktionieren mit jeder zusammenhängenden Sequenz. Slices leihen die zugrundeliegenden Daten und verhindern, dass sie modifiziert oder gedroppt werden, während der Slice existiert.

rust
// string slice: &str
let s = String::from("hello world");
let hello: &str = &s[0..5];   // "hello"
let world: &str = &s[6..];    // "world"
let full: &str = &s[..];      // "hello world"

// array slice: &[T]
let arr = [1, 2, 3, 4, 5];
let mid: &[i32] = &arr[1..4]; // [2, 3, 4]

// function accepting slices (idiomatic)
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

Lifetimes

Lifetimes sagen dem Compiler, wie lange Referenzen gültig sind. Die 'a-Annotation ändert Lifetimes nicht – sie beschreibt Beziehungen. 'static ist eine spezielle Lifetime, die das gesamte Programm dauert (String-Literale haben sie). Die meiste Code verwendet Lifetime-Elision (Compiler leitet ab). Du brauchst explizite Lifetimes, wenn: eine Funktion eine Referenz zurückgibt oder ein Struct eine Referenz hält.

rust
// explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y's lifetimes

let s1 = String::from("long string");
let s2 = String::from("hi");
let result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);

// struct holding references needs lifetime
struct Excerpt<'a> {
    part: &'a str,
}
let novel = String::from("call me Ishmael. years ago...");
let first_sentence = novel.split('.').next().unwrap();
let ex = Excerpt { part: first_sentence };

Smart Pointers

Box<T> bewegt Daten auf den Heap (einzelner Owner). Rc<T> ermöglicht geteiltes Ownership über Reference Counting (nur Single-Threaded). Arc<T> ist die Thread-sichere Version, die Atomics verwendet. RefCell<T> bewegt Borrow-Checking zur Runtime und erlaubt Mutation durch geteilte Referenzen (Interior Mutability). Verwende Box für rekursive Typen, Rc/Arc für Graph-ähnliche Strukturen, RefCell, wenn du geteilte Daten mutieren musst.

rust
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;

// Box<T>: heap allocation, single owner
let b = Box::new(5); // 5 lives on heap
println!("{}", b);   // dereferenced automatically

// Rc<T>: reference counting, multiple owners (single-threaded)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments ref count
println!("count: {}", Rc::strong_count(&a)); // 2

// Arc<T>: atomic Rc, thread-safe
let arc = Arc::new(vec![1, 2, 3]);

// RefCell<T>: interior mutability (runtime borrow check)
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // mutable borrow checked at runtime
07

Structs, Enums & Traits

Structs definieren

Structs gruppieren verwandte Felder. Named-Field-Structs sind am häufigsten. Tuple-Structs sind nützlich, wenn Feldnamen nicht sinnvoll sind (Color, Point). Unit-Structs haben keine Daten und werden verwendet, um Traits zu implementieren. Die ..-Syntax kopiert unspezifizierte Felder von einer anderen Instanz. Structs sind Stack-allokiert, außer sie enthalten Heap-Typen (String, Vec, Box).

rust
// named-field struct
struct User {
    name: String,
    age: u32,
    active: bool,
}

let u = User {
    name: String::from("Alice"),
    age: 30,
    active: true,
};

// field init shorthand
let name = String::from("Bob");
let u2 = User { name, age: 25, active: true };

// update syntax (copy remaining fields from another)
let u3 = User { age: 40, ..u2 };

// tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);

// unit struct (no fields, useful for traits)
struct AlwaysEqual;

Methoden mit impl

Methoden stehen in impl-Blöcken. &self leiht unveränderlich, &mut self leiht veränderlich, self übernimmt Ownership (konsumiert). Assoziierte Funktionen (kein self-Parameter) sind wie statische Methoden – aufgerufen mit Type::function(). Self ist ein Alias für den Typ. Mehrere impl-Blöcke sind erlaubt, nützlich, um Methoden nach Anliegen oder bedingter Kompilierung zu teilen.

rust
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // associated function (constructor, no &self)
    fn new(w: f64, h: f64) -> Self {
        Rectangle { width: w, height: h }
    }

    // method (borrows self)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // mutable method
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // consuming method (takes ownership)
    fn into_square(self) -> Rectangle {
        let side = (self.width + self.height) / 2.0;
        Rectangle { width: side, height: side }
    }
}

let mut r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area()); // 50
r.scale(2.0);
let sq = r.into_square(); // r consumed

Enums & Pattern-Matching

Enums in Rust sind algebraische Datentypen – jede Variante kann unterschiedliche Daten tragen. Das macht sie viel mächtiger als C-Enums. Pattern-Matching mit match destrukturiert Varianten und extrahiert ihre Daten. Verwende Enums, wenn ein Wert eine von mehreren distincten Formen sein kann. Das matches!-Makro ist eine Kurzform für Single-Pattern-Matching, das bool zurückgibt.

rust
// enum with data (algebraic data type)
enum Message {
    Quit,                          // no data
    Move { x: i32, y: i32 },       // named fields
    Write(String),                 // tuple variant
    ChangeColor(i32, i32, i32),    // tuple variant
}

// pattern matching with destructuring
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("RGB({}, {}, {})", r, g, b),
    }
}

process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));

// enums can have methods too
impl Message {
    fn is_quit(&self) -> bool {
        matches!(self, Message::Quit)
    }
}

Option & Result

Option<T> ersetzt null – du musst den None-Case explizit behandeln, was NullPointerException-artige Bugs eliminiert. Result<T, E> ist für Operationen, die fehlschlagen können. Beide haben reiche Methoden: map (transformieren), and_then (verketten), unwrap_or (Default), is_some/is_ok (prüfen). Der ?-Operator auf Result propagiert Errors automatisch. Diese zwei Typen sind das Rückgrat von Rusts Fehlerbehandlung.

rust
// Option<T>: Some(value) or None (replaces null)
fn find_user(id: i32) -> Option<String> {
    if id == 1 { Some(String::from("Alice")) }
    else { None }
}

let user = find_user(1);
match user {
    Some(name) => println!("Found: {}", name),
    None => println!("Not found"),
}

// convenient methods
let name = find_user(1).unwrap_or("Anonymous".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).and_then(|n| Some(n.len()));

// Result<T, E>: Ok(value) or Err(error)
fn parse_num(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse()
}

match parse_num("42") {
    Ok(n) => println!("Parsed: {}", n),
    Err(e) => println!("Error: {}", e),
}

Traits & Trait-Bounds

Traits definieren geteiltes Verhalten (wie Interfaces in anderen Sprachen). Typen implementieren Traits mit 'impl Trait for Type'. Traits können Default-Methoden-Implementierungen haben. Trait-Bounds (<T: Trait>) beschränken Generics auf Typen, die bestimmte Traits implementieren. Die 'where'-Klausel verbessert die Lesbarkeit für komplexe Bounds. Traits ermöglichen Polymorphismus über sowohl statischen Dispatch (Generics) als auch dynamischen Dispatch (Trait-Objekte &dyn Trait).

rust
// define a trait (interface)
trait Summary {
    fn summarize(&self) -> String;

    // default method
    fn author(&self) -> String {
        String::from("Unknown")
    }
}

struct Article { title: String, content: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
    // author() uses default implementation
}

let a = Article {
    title: "Rust".into(),
    content: "Great".into(),
};
println!("{}", a.summarize());
println!("Author: {}", a.author()); // Unknown

// generic with trait bound
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// multiple bounds with +, or where clause
fn display<T: Summary + std::fmt::Display>(item: &T) {}
// or: fn display<T>(item: &T) where T: Summary + std::fmt::Display {}

Derive-Makros & Trait-Objekte

#[derive(...)] auto-implementiert häufige Traits: Debug (Debug-Drucken), Clone (Deep Copy), PartialEq/Eq (==-Vergleich), Hash (für HashMap-Schlüssel), Copy (Stack-Kopie statt Move). Trait-Objekte (&dyn Trait oder Box<dyn Trait>) ermöglichen Runtime-Polymorphismus über eine Vtable bei kleinen Performance-Kosten. Verwende Generics für statischen Dispatch (Zero-Cost), wenn möglich, Trait-Objekte, wenn du heterogene Collections brauchst.

rust
// derive common traits automatically
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();          // Clone
println!("{:?}", p1);          // Debug
println!("{}", p1 == p2);      // PartialEq -> true

// trait object: dynamic dispatch
trait Animal {
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;

impl Animal for Dog { fn sound(&self) -> String { "Woof".into() } }
impl Animal for Cat { fn sound(&self) -> String { "Meow".into() } }

// Vec of trait objects (dynamic dispatch via vtable)
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];
for a in &animals {
    println!("{}", a.sound());
}
08

Fehlerbehandlung

Result & ?-Operator

Der ?-Operator ist der idiomatische Weg, Errors zu propagieren. Bei Ok(v) entpackt er zu v. Bei Err(e) gibt er Err(e) aus der Funktion sofort zurück. ? konvertiert auch Error-Typen über das From-Trait, sodass Funktionen, die Box<dyn Error> zurückgeben, ? auf jedem Error-Typ verwenden können. Auf Option gibt ? früh None zurück. Das macht Fehlerbehandlung prägnant ohne Sicherheitsverlust.

rust
use std::fs;
use std::io;
use std::num::ParseIntError;

// ? propagates errors: if Err, return early; if Ok, unwrap
fn read_config(path: &str) -> Result<i32, io::Error> {
    let content = fs::read_to_string(path)?; // ? on io::Result
    Ok(content.len() as i32)
}

// ? converts error types via From
fn parse_and_read(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;     // io::Error -> Box
    let n: i32 = content.trim().parse()?;         // ParseIntError -> Box
    Ok(n)
}

// ? on Option too
fn first_char(s: &str) -> Option<char> {
    s.lines().next()?.chars().next()
}

Panic vs Result

Verwende panic! für nicht behebbare Errors (Bugs, verletzte Invarianten) – es zeigt einen Programmierfehler an. Verwende Result für erwartete, behebbare Fehlschläge (Benutzereingabe, Datei-I/O, Netzwerk). unwrap()/expect() panikt bei Error – akzeptabel in Tests, Prototypen oder wenn du beweisen kannst, dass der Wert gültig ist. In Produktionscode bevorzuge richtige Fehlerbehandlung mit ? und match.

rust
// panic: unrecoverable error, crashes the program
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Division by zero!"); // unwinds stack
    }
    a / b
}

// Result: recoverable error, caller decides
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}

// unwrap/expect: panic on Err (use sparingly)
let n: i32 = "42".parse().unwrap();       // panics on error
let n2: i32 = "42".parse().expect("valid number"); // panic with msg

// when to panic vs Result:
// panic: bugs, invariant violations, impossible states
// Result: expected failures (file not found, parse error)

Benutzerdefinierte Error-Typen

Benutzerdefinierte Error-Typen geben dir typsichere, strukturierte Fehlerbehandlung. Implementiere Display (menschenlesbar) und Error (für Source-Chaining). Implementiere From für jeden zugrundeliegenden Error, sodass ? automatisch konvertiert. Bibliotheken wie thiserror (Derive-Makro) oder anyhow (dynamische Error-Boxen) reduzieren Boilerplate. Verwende thiserror für Bibliotheken, anyhow für Anwendungen.

rust
use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
}

// implement Display (required by Error)
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(name) => write!(f, "Not found: {}", name),
        }
    }
}

// implement Error
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            _ => None,
        }
    }
}

// From impls for ? to work automatically
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

Results matchen & kombinieren

Result-Kombinatoren erlauben funktionale Fehlerbehandlung ohne match. map transformiert den Ok-Wert, map_err transformiert den Error, and_then verketten fehlgeschlagene Operationen (flatMap). unwrap_or bietet einen Default bei Error. is_ok/is_err prüfen ohne zu konsumieren. Diese Methoden machen Fehlerbehandlungs-Ketten lesbar und vermeiden tiefe Verschachtelung.

rust
// match on Result
let result: Result<i32, &str> = Ok(42);
match result {
    Ok(n) => println!("Got: {}", n),
    Err(e) => println!("Error: {}", e),
}

// combinators
let r1: Result<i32, &str> = Ok(5);
let doubled = r1.map(|n| n * 2);           // Ok(10)
let mapped_err = r1.map_err(|e| format!("{}", e));

// and_then: chain fallible operations
let r2 = r1.and_then(|n| if n > 0 { Ok(n * 2) } else { Err("negative") });

// unwrap_or family
let val = r1.unwrap_or(0);        // 5 or 0
let val2 = r1.unwrap_or_default(); // 5 or T::default()
let val3 = r1.unwrap_or_else(|_| 0); // lazy default

// check variants
println!("{}", r1.is_ok());  // true
println!("{}", r1.is_err()); // false

Errors mit From konvertieren

Der ?-Operator verwendet From, um Errors zu konvertieren. Box<dyn Error> implementiert From für alle Standard-Errors, was es zu einer bequemen Catch-All macht. Die anyhow-Crate bietet anyhow::Result, das Kontext hinzufügt (z.B. .context("failed to read config")?). Für Bibliotheken definiere ein spezifisches Error-Enum mit thiserror; für Anwendungen verwende anyhow für Einfachheit.

rust
use std::fs;
use std::num::ParseIntError;

// Without From: manual conversion needed
fn manual(path: &str) -> Result<i32, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| e.to_string())?;  // manual convert
    let n: i32 = content.trim().parse()
        .map_err(|e| e.to_string())?;  // manual convert
    Ok(n)
}

// With Box<dyn Error>: auto-converts via From
fn boxed(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;  // auto-converts
    let n: i32 = content.trim().parse()?;      // auto-converts
    Ok(n)
}

// anyhow::Result (from anyhow crate) is ergonomic for apps
// fn anyhow_fn() -> anyhow::Result<i32> {
//     let n: i32 = something()?; // any error auto-converted
//     Ok(n)
// }
09

Module & Crates

Modulsystem

Rusts Modulsystem organisiert Code. 'mod' deklariert ein Modul (inline oder via Datei). 'pub' macht Items öffentlich (Standard ist privat). 'use' erstellt Shortcuts zu Pfaden. 'pub use' re-exportiert Items (nützlich für API-Design). Das Dateisystem spiegelt den Modulbaum: mod network kann src/network.rs oder src/network/mod.rs sein. Crate-Root ist lib.rs (Bibliotheken) oder main.rs (Binaries).

rust
// mod.rs or mod declaration in lib.rs/main.rs
// File: src/lib.rs
mod network {
    pub mod connection {
        pub fn connect() -> bool { true }
        fn disconnect() {} // private
    }
}

// use: bring paths into scope
use network::connection::connect;

// calling with full path
fn main() {
    network::connection::connect();
    connect(); // after 'use'
}

// re-export with pub use
pub use network::connection::connect as open_connection;

// nested file: src/network/connection.rs
// mod network; in lib.rs loads src/network/mod.rs
// which can have: pub mod connection;

Privacy & Sichtbarkeit

Privacy in Rust ist modulbezogen. Items sind standardmäßig privat – nur zugänglich in ihrem definierenden Modul und Nachkommen. 'pub' macht sie öffentlich. 'pub(crate)' beschränkt auf die aktuelle Crate (nützlich für Bibliotheks-Internas). 'pub(super)' beschränkt auf das Eltern-Modul. Struct-Felder haben individuelle Sichtbarkeit – ein pub-Struct kann private Felder haben und erfordert einen Konstruktor.

rust
mod my_module {
    // private by default
    fn internal_helper() {}

    // public: accessible from outside
    pub fn public_api() {
        internal_helper(); // can call private within module
    }

    // pub(crate): visible within this crate only
    pub(crate) fn crate_wide() {}

    // pub(super): visible to parent module
    pub(super) fn parent_visible() {}

    // struct fields are private even if struct is pub
    pub struct Config {
        pub name: String,    // public field
        secret: String,      // private field
    }

    // enum variants inherit the enum's visibility
    pub enum Status {
        Active,  // public because Status is public
        Inactive,
    }
}

Use-Statements & Aliasing

use-Statements bringen Items in den Scope und reduzieren Pfad-Ausführlichkeit. Gruppierte Imports (use std::io::{self, Read}) sind sauberer als mehrere Zeilen. Traits müssen im Scope sein, um ihre Methoden zu verwenden – deshalb brauchst du manchmal 'use std::io::Read', selbst wenn du Read nicht namentlich referenzierst. Glob-Imports (*) werden außer für Preludes abgeraten.

rust
// basic use
use std::collections::HashMap;
use std::fs::read_to_string;

// grouped use
use std::io::{self, Read, Write, BufRead};
// equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;

// aliasing with 'as'
use std::collections::HashMap as Map;

// glob import (use sparingly)
use std::prelude::v1::*;

// bringing trait methods into scope
use std::io::Read; // now .read() method is available
let mut f = std::fs::File::open("x")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?; // Read trait method

Cargo & externe Crates

Cargo ist Rusts Paketmanager und Build-System. Abhängigkeiten stehen in Cargo.toml unter [dependencies]. Features aktivieren optionale Funktionalität (reduziert Compile-Zeit/Binary-Größe). 'cargo add' bearbeitet Cargo.toml automatisch. Crates werden auf crates.io veröffentlicht. Edition (2021) kontrolliert Sprach-Features. Cargo behandelt Kompilierung, Testing, Dokumentation und Veröffentlichung.

rust
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"

# in Rust code
use serde::{Serialize, Deserialize};
use rand::Rng;

#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }

fn main() {
    let mut rng = rand::thread_rng();
    let n: i32 = rng.gen_range(1..=100);
    println!("{}", n);
}

# CLI commands:
# cargo new my_app      # create new project
# cargo build           # compile
# cargo run             # compile + run
# cargo test            # run tests
# cargo add serde       # add dependency
# cargo update          # update dependencies

Testing

Tests verwenden das #[test]-Attribut. assert_eq!/assert_ne! vergleichen Werte. #[should_panic] verifiziert, dass ein Panic auftritt. Tests können Result für Error-basierte Assertions zurückgeben. #[cfg(test)] stellt sicher, dass das Tests-Modul nur während des Testens kompiliert. Unit-Tests leben neben Code; Integration-Tests gehen ins tests/-Verzeichnis. Führe aus mit 'cargo test'. Verwende #[ignore], um unzuverlässige Tests zu überspringen.

rust
// Unit tests in same file (convention: tests module)
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 3), 6);
    }

    #[test]
    fn test_with_message() {
        let result = add(1, 1);
        assert_eq!(result, 2, "Expected 2, got {}", result);
    }

    #[test]
    #[should_panic(expected = "division by zero")]
    fn test_panic() {
        panic!("division by zero");
    }

    #[test]
    fn test_result() -> Result<(), String> {
        if add(2, 2) == 4 { Ok(()) }
        else { Err(String::from("math is broken")) }
    }
}

// Integration tests: tests/integration_test.rs
// Run: cargo test
10

Nebenläufigkeit & Datei-I/O

Threads

std::thread::spawn erstellt OS-Threads. Die Closure muss 'move' sein, wenn sie Variablen erfasst, und überträgt Ownership an den Thread (verhindert Use-After-Free). join() blockiert, bis der Thread abgeschlossen ist, und gibt ein Result zurück. Rusts Ownership-System verhindert Data Races zur Compile-Zeit – du kannst nicht veränderliche Daten zwischen Threads teilen ohne Synchronisation (Arc<Mutex<T>>).

rust
use std::thread;
use std::time::Duration;

// spawn a thread
let handle = thread::spawn(|| {
    for i in 0..5 {
        println!("spawned thread: {}", i);
        thread::sleep(Duration::from_millis(10));
    }
});

// main thread continues
for i in 0..3 {
    println!("main thread: {}", i);
}

// wait for spawned thread to finish
handle.join().unwrap();

// move closure: transfer ownership to thread
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
    println!("got data: {:?}", data); // data moved here
});
h.join().unwrap();

Channels (Message Passing)

Channels ermöglichen Thread-Kommunikation über Message Passing (Go's Motto: 'Teile Speicher durch Kommunikation'). mpsc erlaubt mehrere Sender (tx.clone()), aber einen Empfänger. send() gibt Result zurück (Err, wenn Empfänger gedroppt). Der Empfänger implementiert Iterator, also funktionieren for-Schleifen natürlich. Für mehrere Empfänger verwende crossbeam-channel oder Async-Channels. Message Passing vermeidet die Komplexität geteilten veränderlichen Zustands.

rust
use std::sync::mpsc;
use std::thread;

// mpsc: multiple producer, single consumer
let (tx, rx) = mpsc::channel();

// clone transmitter for multiple producers
let tx2 = tx.clone();

thread::spawn(move || {
    let vals = vec!["a", "b", "c"];
    for v in vals {
        tx.send(v).unwrap();
    }
});

thread::spawn(move || {
    tx2.send("from tx2").unwrap();
});

// receiver is an iterator
for received in rx {
    println!("Got: {}", received);
}

Mutex & Arc (Geteilter Zustand)

Arc (Atomic Reference Counted) ermöglicht geteiltes Ownership über Threads (Thread-sicheres Rc). Mutex bietet exklusiven Zugriff – lock() blockiert, bis erworben, und gibt einen MutexGuard zurück, der das Lock bei Drop freigibt. RwLock erlaubt mehrere Leser oder einen Schreiber. Die Kombination Arc<Mutex<T>> ist das Standard-Pattern für geteilten veränderlichen Zustand. unwrap() auf lock() behandelt Poison (ein Thread panikte während er das Lock hielt).

rust
use std::sync::{Arc, Mutex};
use std::thread;

// Arc: thread-safe reference counting
// Mutex: mutual exclusion lock
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        // lock() returns MutexGuard, auto-unlocks when dropped
        let mut num = counter.lock().unwrap();
        *num += 1;
    }); // lock released here
    handles.push(handle);
}

for h in handles { h.join().unwrap(); }

println!("Result: {}", *counter.lock().unwrap()); // 10

// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let data = RwLock::new(5);
let r1 = data.read().unwrap();  // shared read
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // would block until r1, r2 dropped

Datei-I/O

fs::read_to_string ist bequem für kleine Dateien. Für große Dateien verwende BufReader/BufWriter, um System-Calls zu reduzieren. Read/Write-Traits bieten Low-Level-Byte-Operationen. BufRead fügt lines() und read_line() für Text hinzu. Behandle immer Errors mit ? (Dateien können fehlen, Berechtigungen verweigert, Festplatte voll). flush() stellt sicher, dass gepufferte Daten das OS erreichen (aber nicht zwingend die Festplatte).

rust
use std::fs;
use std::io::{Read, Write, BufReader, BufWriter};
use std::fs::File;

// read entire file
let content = fs::read_to_string("file.txt")?;
fs::write("output.txt", "Hello")?;

// buffered read (efficient for large files)
let file = File::open("file.txt")?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;

// line by line
use std::io::BufRead;
let file = File::open("file.txt")?;
for line in BufReader::new(file).lines() {
    println!("{}", line?);
}

// buffered write
let file = File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "Line 1")?;
writeln!(writer, "Line 2")?;
writer.flush()?; // ensure written to disk

Async/Await (Tokio)

Async/await ermöglicht effiziente Nebenläufigkeit ohne OS-Threads – Tasks laufen auf einem Thread-Pool und yielden an .await-Punkten. tokio ist die beliebteste Async-Runtime. async fn gibt ein Future zurück, das .awaitet werden muss. tokio::join! führt Futures gleichzeitig aus und wartet auf alle. tokio::select! lässt Futures rennen. Async ist ideal für I/O-gebundene Arbeit (Netzwerk, Dateien); verwende Threads für CPU-gebundene Arbeit. Das .await blockiert nicht den Thread – es gibt Kontrolle an die Runtime zurück.

rust
use tokio::{fs, task, time};
use std::time::Duration;

// async fn returns a Future
async fn fetch_data() -> String {
    time::sleep(Duration::from_secs(1)).await;
    String::from("data")
}

// spawn concurrent tasks
#[tokio::main]
async fn main() {
    // run tasks concurrently
    let (a, b, c) = tokio::join!(
        fetch_data(),
        fetch_data(),
        fetch_data()
    );
    println!("{} {} {}", a, b, c);

    // spawn a background task
    let handle = tokio::spawn(async {
        let data = fs::read_to_string("file.txt").await.unwrap();
        println!("Read {} bytes", data.len());
    });
    handle.await.unwrap();

    // select: first to complete wins
    tokio::select! {
        val = fetch_data() => println!("Got: {}", val),
        _ = time::sleep(Duration::from_millis(500)) => {
            println!("Timeout!");
        }
    }
}
11

Lifetimes Deep Dive

Benannte Lifetimes in Funktionen

Lifetimes sind des Compilers Weg, Referenz-Gültigkeit zu verfolgen. Das 'a ist ein generischer Lifetime-Parameter – es ändert nicht das Runtime-Verhalten, nur Compile-Zeit-Prüfung. Wenn eine Funktion mehrere Referenzen nimmt und eine zurückgibt, musst du Lifetimes annotieren, damit der Compiler weiß, dass die zurückgegebene Referenz ihre Inputs nicht überlebt. Das verhindert Dangling-Pointer zur Compile-Zeit.

rust
// Lifetimes tell the compiler how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y

let s1 = String::from("long string");
let result;
{
    let s2 = String::from("hi");
    result = longest(s1.as_str(), s2.as_str());
    // result valid here
    println!("{}", result);
}
// result NOT valid here — s2 is dropped

Lifetime-Elision-Regeln

Lifetime-Elision-Regeln lassen dich explizite Lifetime-Annotationen in häufigen Fällen weglassen. Regel 1 weist jeder Referenz-Parameter eine distinct Lifetime zu. Regel 2 weist diese Lifetime der Ausgabe zu, wenn es genau eine Input-Referenz gibt. Regel 3 gilt für Methoden – die Ausgabe bekommt &self's Lifetime. Wenn keine dieser Regeln alle Referenzen auflöst, musst du explizite Lifetimes schreiben. Die meiste idiomatische Rust-Code braucht selten explizite Lifetime-Annotationen.

rust
// The compiler applies 3 elision rules automatically:
// 1. Each input reference gets its own lifetime
// 2. If one input lifetime, output gets that lifetime
// 3. If &self/&mut self, output gets self's lifetime

// These DON'T need explicit annotations (rule 2):
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' { return &s[0..i]; }
    }
    &s[..]
}

// This NEEDS annotation (multiple inputs, rule doesn't apply):
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

Die 'static-Lifetime

'static ist die längste Lifetime – sie dauert das gesamte Programm. Alle String-Literale haben diese Lifetime, weil sie im Binary eingebettet sind. Wenn du T: 'static als Bound siehst, bedeutet das nicht, dass T für immer leben muss – es bedeutet, dass T keine Referenzen kürzer als 'static enthalten darf (d.h., owned Typen wie String, Vec, i64 erfüllen das immer). Threads erfordern 'static, weil sie die aufrufende Funktion überleben können.

rust
// 'static means the reference lives for the entire program
let s: &'static str = "I live forever";
// All string literals are &'static str

// Static variables (global, program-wide)
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);

// 'static in bounds: T: 'static means T contains no
// non-static references (owned data always satisfies this)
fn spawn_thread<T: Send + 'static>(t: T) {
    std::thread::spawn(move || {
        drop(t);
    });
}

Lifetimes in Structs

Wenn ein Struct eine Referenz hält (kein owned Typ), braucht es einen Lifetime-Parameter, um zu deklarieren, wie lange diese Referenz gültig ist. Die Struct-Instanz kann die Daten, die sie leiht, nicht überleben. Das ist häufig bei Zero-Copy-Parser, Iteratoren über geliehene Daten und Views. Die Lifetime muss sowohl auf dem Struct als auch seinem impl-Block deklariert sein. Bevorzuge owned Daten (String, Vec), außer du hast einen spezifischen Grund zu leihen.

rust
// Structs holding references need lifetime annotations
struct Parser<'a> {
    text: &'a str,
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(text: &'a str) -> Self {
        Parser { text, pos: 0 }
    }
    fn peek(&self) -> Option<char> {
        self.text[self.pos..].chars().next()
    }
    fn advance(&mut self) {
        self.pos += 1;
    }
}

// The struct cannot outlive the text it borrows
let text = String::from("hello");
let mut p = Parser::new(&text);
p.advance();

Mehrere Lifetimes & Subtyping

Funktionen mit mehreren Referenzen, die unterschiedlich interagieren, brauchen mehrere Lifetime-Parameter. Lifetime-Subtyping bedeutet, dass eine längere Lifetime für eine kürzere substituieren kann (Kovarianz) – &'static kann überall verwendet werden, wo &'a erwartet wird. Deshalb ist 'static ein Subtyp aller Lifetimes. Verwende mehrere Lifetimes, wenn die Lifetime der Ausgabe nur von einigen Inputs abhängt, was dem Compiler mehr Flexibilität und dem Aufrufer weniger Constraints gibt.

rust
// Multiple lifetime parameters
fn parse<'src, 'ctx>(src: &'src str, ctx: &'ctx Context) -> &'src str {
    // returns something tied to src, not ctx
    src
}

// 'static: 'a is always true (static outlives everything)
fn static_ref<'a>(_: &'a str) -> &'static str {
    "constant"  // 'static coerces to 'a
}

// Variance: &'long can be used where &'short is expected
// (covariance) — longer lifetime is a subtype
fn use_ref<'short>(r: &'short str) {}
let long: &'static str = "hi";
use_ref(long);  // OK: 'static coerces to any 'short
12

Smart Pointers (Box, Rc, Arc, RefCell)

Box<T> — Heap-Allokation

Box<T> ist Rusts einfachster Smart Pointer – er heap-allokiiert einen Wert mit einzelnem Ownership. Verwende ihn für rekursive Typen (deren Größe zur Compile-Zeit nicht bekannt sein kann), große Werte, die du nicht auf dem Stack bewegen willst, und Trait-Objekte (Box<dyn Trait>) für dynamischen Dispatch. Box dereft zu T, also verhält es sich wie der innere Wert. Es hat im Wesentlichen Zero-Overhead über die Heap-Allokation hinaus.

rust
// Box moves data to the heap (single owner)
let b = Box::new(5);
println!("{}", b);  // derefs automatically

// Recursive types NEED Box (size unknown at compile time)
enum List {
    Cons(i32, Box<List>),
    Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

// Trait objects (dynamic dispatch)
let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }

// Box has zero runtime overhead when dereferencing

Rc<T> — Reference Counting (Single-Thread)

Rc<T> (Reference Counted) ermöglicht mehrfaches Ownership für Single-Threaded-Szenarien. Rc::clone inkrementiert den Reference-Count statt Daten zu kopieren – günstiger Pointer-Copy. Der Wert wird gedroppt, wenn das letzte Rc gedroppt wird. Verwende Rc für geteilte Graph-Knoten, Parent-Child-Bäume oder jede Struktur, wo mehrere Teile dieselben Daten besitzen müssen. Rc ist NICHT Thread-sicher – verwende Arc für Multithreading. Rc ist unveränderlich – du kannst nicht direkt durch es mutieren.

rust
use std::rc::Rc;

// Rc allows multiple owners via reference counting
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a);  // increments count, doesn't copy
let c = Rc::clone(&a);

println!("count = {}", Rc::strong_count(&a));  // 3
// Data dropped when count reaches 0

// Shared graph/tree structures
struct Node {
    children: Vec<Rc<Node>>,
    value: i32,
}
let leaf = Rc::new(Node { children: vec![], value: 1 });
let branch = Rc::new(Node {
    children: vec![Rc::clone(&leaf)],
    value: 2,
});

Arc<T> — Atomic Reference Counting (Thread-Safe)

Arc<T> (Atomically Reference Counted) ist das Thread-sichere Gegenstück von Rc. Es verwendet atomare Operationen für Reference-Counting, was es sicher macht, über Threads zu teilen. Der Trade-off ist etwas mehr Overhead als Rc. Verwende Arc, wann immer du geteiltes Ownership über mehrere Threads brauchst. Um geteilte Daten zu mutieren, kombiniere Arc mit Mutex (für exklusiven Zugriff) oder RwLock (für lese-lastigen Zugriff). Arc::clone ist günstig – es inkrementiert nur einen atomaren Zähler.

rust
use std::sync::Arc;
use std::thread;

// Arc: thread-safe version of Rc (atomic ref counting)
let data = Arc::new(vec![1, 2, 3, 4, 5]);

let handles: Vec<_> = (0..3).map(|i| {
    let data = Arc::clone(&data);  // atomic increment
    thread::spawn(move || {
        println!("Thread {} sees: {:?}", i, *data);
    })
}).collect();

for h in handles { h.join().unwrap(); }

// Arc is slightly slower than Rc due to atomic operations
// Only use Arc when actually sharing across threads

RefCell<T> — Interior Mutability

RefCell<T> bietet Interior Mutability – du kannst durch eine geteilte Referenz mutieren, mit Borrow-Regeln zur Runtime statt Compile-Zeit erzwungen. borrow() gibt eine unveränderliche Referenz zurück, borrow_mut() eine veränderliche. Das Verletzen der Regeln (z.B. zwei veränderliche Borrows) verursacht einen Panic zur Runtime. Verwende RefCell, wenn der Compiler Borrow-Safety nicht beweisen kann (z.B. Graph-Strukturen, Mock-Objekte in Tests). Rc<RefCell<T>> ist das klassische Pattern für veränderliche geteilte Single-Threaded-Daten.

rust
use std::cell::RefCell;

// RefCell moves borrow checking to RUNTIME
let cell = RefCell::new(vec![1, 2, 3]);

// Multiple immutable borrows OR one mutable borrow
{
    let mut borrowed = cell.borrow_mut();
    borrowed.push(4);
}  // borrow released here

{
    let r1 = cell.borrow();     // OK
    let r2 = cell.borrow();     // OK — multiple immutable
    println!("{:?} {:?}", r1, r2);
}

// cell.borrow_mut() while r1 alive → PANIC at runtime
// Combine with Rc: Rc<RefCell<T>> for mutable shared graphs

Weak<T> — Referenz-Zyklen brechen

Weak<T> ist eine nicht-owning Referenz, die den starken Reference-Count nicht beeinflusst. Essenziell, um Referenz-Zyklen zu brechen: Wenn ein Parent Children (Rc) besitzt und Children den Parent (Rc) besitzen, wird nie etwas freigegeben – ein Speicherleck. Die Lösung ist, die Back-Reference Weak zu machen. upgrade() gibt Option<Rc<T>> zurück – None, wenn der Wert bereits gedroppt wurde. Verwende Weak für Child→Parent-Links, Caches und Observer-Patterns, wo du Daten nicht am Leben halten willst.

rust
use std::rc::{Rc, Weak, RefCell};

// Weak references don't contribute to the strong count
// — prevents memory leaks in cycles
struct Node {
    parent: RefCell<Weak<Node>>,        // weak: child → parent
    children: RefCell<Vec<Rc<Node>>>,   // strong: parent → children
}

let leaf = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

// Upgrade returns Option — parent may already be dropped
if let Some(p) = leaf.parent.borrow().upgrade() {
    println!("parent exists");
}
13

Trait-Objekte & Dynamischer Dispatch

dyn Trait — Dynamischer Dispatch

dyn Trait ermöglicht dynamischen Dispatch – der konkrete Typ wird zur Compile-Zeit gelöscht und Methodenaufrufe gehen über eine Vtable zur Runtime. Das lässt dich heterogene Typen in einer einzigen Collection speichern (Vec<Box<dyn Animal>>). Der Trade-off: kleine Runtime-Kosten (Vtable-Indirection, kein Inlining) und der Typ kann zur Compile-Zeit nicht bekannt sein. Verwende Trait-Objekte, wenn die Menge konkreter Typen zur Compile-Zeit nicht bekannt ist oder wenn du verschiedene Typen gruppieren musst.

rust
trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Woof".into() }
}
impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Meow".into() }
}

// dyn Trait = erased type, runtime dispatch via vtable
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog { name: "Rex".into() }),
    Box::new(Cat { name: "Whiskers".into() }),
];
for a in &animals {
    println!("{} says {}", a.name(), a.sound());
}

Object-Safety-Regeln

Ein Trait ist nur dann Object-safe, wenn der Compiler eine Vtable dafür bauen kann. Zwei Regeln: (1) Methoden dürfen nicht Self zurückgeben (der konkrete Typ ist gelöscht, also kann er nicht bekannt sein), und (2) Methoden dürfen keine generischen Typparameter haben (die Vtable bräuchte einen Eintrag für jeden möglichen Typ). Traits mit Sized als Supertrait sind ebenfalls nicht Object-safe. Wenn du Object-Safety brauchst, refaktoriere Self-zurückgebende Methoden, um Box<dyn Trait> zurückzugeben, oder verwende eine separate Factory-Funktion.

rust
// Object-safe traits CAN be used as dyn Trait:
trait Draw {
    fn draw(&self);  // OK: &self, no generics
}

// NOT object-safe:
trait Bad {
    fn create() -> Self;        // returns Self — needs known type
    fn process<T>(&self, x: T); // generic method — vtable can't cover all T
    const SIZE: usize;          // associated const (sometimes OK)
}

// Workaround for Self returns — use a factory or Box<Self>
trait GoodFactory {
    fn new_boxed() -> Box<dyn GoodFactory>;
}

// Sized bound makes trait non-object-safe
// trait Foo: Sized {}  // NOT object-safe

Trait-Objekte vs Generics

Generics verwenden Monomorphisierung – der Compiler generiert eine separate Kopie der Funktion für jeden konkreten Typ, was statischen Dispatch und volle Optimierung (Inlining) ermöglicht. Das hat Zero Runtime-Cost, erhöht aber die Binary-Größe. Trait-Objekte (dyn) verwenden eine einzige Funktion mit Vtable-Lookup – kleinere Binary, aber kleine Runtime-Kosten pro Aufruf. Wähle Generics, wenn Performance wichtig und die Typ-Menge klein/bekannt ist; wähle dyn, wenn du heterogene Collections brauchst oder nicht alle Typen vorab kennst.

rust
// Generics: monomorphization, static dispatch, zero overhead
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
// Each type instantiation creates a separate function:
// max_generic::<i32>, max_generic::<f64>, etc.

// Trait objects: dynamic dispatch, single function
fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> bool {
    // can't return — don't know size at compile time
    a.partial_cmp(b) == Some(std::cmp::Ordering::Less)
}

// Rule of thumb:
// - Few types, performance-critical → generics (static dispatch)
// - Many/unknown types, flexibility needed → dyn (dynamic dispatch)
// - Heterogeneous collections → must use dyn

Any-Trait & Downcasting

Der Any-Trait lässt dich Werte jedes Typs speichern und den konkreten Typ zur Runtime über Downcasting wiederherstellen. downcast_ref::<T>() gibt Option<&T> zurück, downcast::<T>() gibt Result<Box<T>, Box<dyn Any>> zurück. Das ist Rusts Escape-Hatch, wenn du den Typ zur Compile-Zeit wirklich nicht kennst (Plugin-Systeme, dynamische Configs). Bevorzuge jedoch Enums, wenn die Menge möglicher Typen bekannt ist – sie sind sicherer, schneller und idiomatischer. Any verlässt sich auf TypeId, das für alle 'static-Typen implementiert ist.

rust
use std::any::Any;

// Any enables runtime type checking & downcasting
let x: Box<dyn Any> = Box::new(42i32);

// downcast_ref returns Option<&T>
if let Some(n) = x.downcast_ref::<i32>() {
    println!("It's an i32: {}", n);
}

// downcast returns Option<T> (for Box)
let boxed: Box<dyn Any> = Box::new("hello");
if let Ok(s) = boxed.downcast::<&str>() {
    println!("Recovered: {}", s);
}

// Useful for: plugin systems, error types, heterogeneous storage
// Avoid overusing — prefer enums when the type set is known

Default-Trait-Methoden & Supertraits

Traits können Default-Methoden-Implementierungen bereitstellen, die Implementoren überschreiben oder wie-ist verwenden können. Supertraits (trait Named: Shape) erfordern, dass der implementierende Typ auch den Supertrait implementiert – das erstellt eine Hierarchie, wo Named-Typen garantiert area() und describe() haben. Default-Methoden reduzieren Boilerplate und ermöglichen das 'Extension-Method'-Pattern, wo das Hinzufügen einer Methode zu einem Trait automatisch allen existierenden Implementoren zugutekommt, ohne sie zu brechen.

rust
trait Shape {
    fn area(&self) -> f64;
    // Default method — can be overridden
    fn describe(&self) -> String {
        format!("Shape with area {:.2}", self.area())
    }
}

// Supertrait: trait that requires another trait
trait Named: Shape {
    fn name(&self) -> &str;
}

struct Circle { radius: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
}
impl Named for Circle {
    fn name(&self) -> &str { "Circle" }
}

let c = Circle { radius: 2.0 };
println!("{}", c.describe());  // uses default method
14

Makros (Deklarativ & Prozedural)

Deklarative Makros (macro_rules!)

macro_rules! erstellt deklarative Makros, die zur Compile-Zeit über Pattern-Matching expandieren. Die $(...),*-Syntax ist ein Repetition-Matcher – sie matcht null oder mehr komma-getrennte Ausdrücke. $x:expr bedeutet 'matche einen beliebigen Ausdruck und binde ihn an x'. Makros werden vor der Typprüfung expandiert, also können sie Code generieren, der mit jedem Typ funktioniert. Verwende Makros, um Boilerplate zu reduzieren, die Generics nicht handhaben können (z.B. variadische Argumente, Syntax-Erweiterung).

rust
// macro_rules! defines pattern-matching macros
macro_rules! vec_of {
    ($($x:expr),*) => {{
        let mut v = Vec::new();
        $( v.push($x); )*
        v
    }};
}

let nums = vec_of!(1, 2, 3, 4);

// Recursive macro: build a HashMap
macro_rules! hashmap {
    ($($k:expr => $v:expr),*) => {{
        let mut m = std::collections::HashMap::new();
        $( m.insert($k, $v); )*
        m
    }};
}
let config = hashmap!("host" => "localhost", "port" => 8080);

Makro-Fragment-Typen

Fragment-Specifier bestimmen, welche Art von Syntax ein Makro-Argument matcht. :ident matcht Identifiers (Namen), :expr matcht Ausdrücke (Werte), :ty matcht Typen, :block matcht Brace-delimited Blöcke, :stmt matcht Statements, :literal matcht Literale. Den richtigen Specifier zu wählen ist wichtig – :expr ist am häufigsten, aber :ident wird benötigt, wenn du einen Funktions-/Variablennamen erstellen willst. Das Makro-System ist hygienisch: Identifiers, die von Makros eingeführt werden, kollidieren nicht mit umgebendem Code.

rust
// Common fragment specifiers:
macro_rules! build_fn {
    // $name:ident — identifier (function/variable name)
    // $body:block — a block { ... }
    // $ty:ty — a type
    // $expr:expr — an expression
    // $stmt:stmt — a statement
    // $lit:literal — a literal (string, number)
    ($name:ident, $ret:ty, $body:block) => {
        fn $name() -> $ret $body
    };
}

build_fn!(get_answer, i32, { 42 });
println!("{}", get_answer());

// :pat — pattern, :path — module path
// :meta — attribute meta-item, :vis — visibility

Eingebaute Standard-Makros

Rust wird mit vielen eingebauten Makros ausgeliefert. println!/eprintln! drucken nach stdout/stderr. dbg! druckt den Wert eines Ausdrucks mit Datei-/Zeilen-Info – großartig für Debugging (es gibt auch den Wert zurück). assert!/assert_eq!/assert_ne! sind für Tests und Invarianten. todo!/unimplemented! markieren unvollständigen Code mit einem Panic. file!/line!/module! geben Compile-Zeit-Standort-Info. env!/option_env! lesen Umgebungsvariablen zur Compile-Zeit – nützlich für das Einbetten von Versions-Info.

rust
// Formatting & printing
println!("x = {}, y = {:?}", 1, "two");
eprintln!("Error: {}", "oops");     // stderr
format!("{}-{}", "a", "b");          // returns String

// Debug helpers
dbg!(2 + 2);                          // prints [src.rs:1] 2 + 2 = 4
assert!(1 + 1 == 2);                  // panic if false
assert_eq!(2 + 2, 4);                 // panic if not equal
assert_ne!(1, 2);                     // panic if equal

// Code generation
todo!("not implemented yet");         // unimplemented!()
unimplemented!();
panic!("fatal: {}", "reason");

// Environment & file info
println!("{}:{}", file!(), line!());  // src.rs:1
println!("{}", env!("CARGO_PKG_NAME"));

Prozedurale Makros Überblick

Prozedurale Makros (Proc-Makros) sind Rust-Funktionen, die TokenStreams als Eingabe nehmen und TokenStreams als Ausgabe produzieren – volle Code-zu-Code-Transformation. Anders als deklarative Makros können sie beliebige Berechnungen durchführen. Drei Typen: Derive-Makros (fügen Trait-Implementierungen via #[derive] hinzu), Attribut-Makros (annotieren Items) und funktionsähnliche Makros (benutzerdefinierte Syntax wie sqlx::query!). Sie müssen in einer separaten Crate mit proc-macro = true leben. Die syn-Crate parst Rust-Syntax, quote! generiert Code. Beliebte Beispiele: serde, tokio, thiserror.

rust
// Procedural macros are Rust functions that transform code
// Three kinds (must be in a separate crate with proc-macro=true):

// 1. Derive macros — #[derive(MyTrait)]
#[derive(Debug, Clone, MyTrait)]
struct Point { x: f64, y: f64 }

// 2. Attribute macros — #[my_attr]
#[my_attr]
fn function() {}

// 3. Function-like macros — my_macro!(...)
sqlx::query!("SELECT * FROM users");

// Cargo.toml for a proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0"   # parse Rust code
// quote = "1.0" # generate code
// proc-macro2 = "1.0"

Makro-Hygiene & häufige Patterns

Rust-Makros sind hygienisch – Identifiers, die innerhalb eines Makros erstellt werden, existieren in einem separaten 'Syntax-Kontext' und fangen nicht versehentlich Variablen im aufrufenden Scope ab oder shadowen sie. Das verhindert subtile Bugs, wo ein interner Variablenname eines Makros mit dem des Aufrufers kollidiert. stringify! konvertiert jeden Token-Stream zur Compile-Zeit in ein String-Literal (nützlich für Fehlermeldungen). cfg_debug! zeigt ein häufiges Pattern: bedingtes Kompilieren von Code basierend auf Build-Konfiguration über das cfg!-System.

rust
// Hygiene: macro-introduced identifiers don't leak
macro_rules! using_temp {
    ($e:expr) => {
        let temp = $e;  // this 'temp' is distinct from caller's
        println!("{}", temp);
    };
}
let temp = 10;
using_temp!(temp + 5);  // no conflict — hygienic

// Conditional compilation macro
macro_rules! cfg_debug {
    ($($e:tt)*) => {
        #[cfg(debug_assertions)]
        { $($e)* }
    };
}
cfg_debug! {
    println!("Debug mode on");
}

// stringify! converts tokens to a string literal
let s = stringify!(a + b * c);  // "a + b * c"
15

Unsafe Rust

Raw Pointers

Raw Pointers (*const T, *mut T) sind Rusts Escape-Hatch vom Borrow-Checker. Anders als Referenzen können sie null sein, können aliasen (mehrere Pointer auf dieselben Daten) und verfolgen keine Lifetimes. Sie zu erstellen ist sicher, aber das Dereferenzieren erfordert unsafe, weil der Compiler die Gültigkeit nicht garantieren kann. Verwende Raw Pointers für FFI (Schnittstelle mit C), das Implementieren von Low-Level-Datenstrukturen (Linked Lists, Vektoren) und Performance-kritischen Code, wo du Sicherheit manuell sicherstellst. Dokumentiere immer, warum unsafe sicher ist.

rust
// Raw pointers: *const T (immutable) and *mut T (mutable)
let x = 42;
let r1: *const i32 = &x;       // coerce from reference
let r2: *mut i32 = x as *mut i32;  // cast

// Can be null, can alias, no borrow checking
let null: *const i32 = std::ptr::null();

// Creating raw pointers is safe, but DEREFERENCING is unsafe
unsafe {
    println!("r1 = {}", *r1);
}

// Convert between types (transmute-like)
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("0x{:x}", *ptr); }  // little-endian int

Unsafe-Blöcke & -Funktionen

unsafe schaltet den Borrow-Checker nicht aus – es lässt dich fünf spezifische Dinge tun: (1) Raw Pointers dereferenzieren, (2) unsafe-Funktionen aufrufen, (3) unsafe-Traits implementieren, (4) auf static mut zugreifen/modifizieren, (5) auf Union-Felder zugreifen. unsafe-Blöcke machen die unsafe-Operationen explizit und lokal. unsafe fn deklariert, dass der Aufruf der Funktion das Einhalten von Invarianten erfordert, die der Compiler nicht prüfen kann. get_unchecked überspringt Bounds-Checking für Performance – nur sicher, wenn du den Index verifiziert hast. Minimiere die unsafe-Oberfläche und kapsle sie hinter einer sicheren API.

rust
// unsafe block: a localized unsafe region
let ptr: *const i32 = &42;
let val = unsafe { *ptr };

// unsafe fn: the ENTIRE function body is unsafe
unsafe fn dangerous(ptr: *const u8) -> u8 {
    *ptr
}
// Callers must use unsafe block:
let b = 5u8;
unsafe { dangerous(&b) };

// Splitting borrows safely (compiler is conservative)
let mut v = vec![1, 2, 3, 4];
let len = v.len();
unsafe {
    let first = v.get_unchecked(0);   // no bounds check
    let last = v.get_unchecked(len - 1);
    println!("{} {}", first, last);
}

FFI — C-Funktionen aufrufen

FFI (Foreign Function Interface) lässt Rust C-Funktionen aufrufen und umgekehrt. extern "C"-Blöcke deklarieren externe C-Funktionen – sie aufzurufen ist unsafe, weil der Compiler ihr Verhalten nicht verifizieren kann. #[no_mangle] verhindert, dass Rust die Funktion umbenennt, sodass C sie nach Namen finden kann. #[repr(C)] garantiert, dass das Struct-Layout C's Speicher-Layout matcht (Rust kann Felder standardmäßig für Effizienz umordnen). Verwende FFI für System-Calls, Legacy-Bibliotheken und Performance-kritische Bindings. Die bindgen-Crate auto-generiert FFI-Deklarationen aus C-Headern.

rust
// extern "C" declares foreign functions
extern "C" {
    fn abs(x: i32) -> i32;
}

fn main() {
    let x = -5;
    let positive = unsafe { abs(x) };
    println!("{}", positive);  // 5
}

// Exporting Rust functions to C
#[no_mangle]  // prevent name mangling
pub extern "C" fn add(a: i64, b: i64) -> i64 {
    a + b
}

// C-compatible struct
#[repr(C)]
struct Point { x: f64, y: f64 }

Unsafe-Traits implementieren

Unsafe-Traits (wie Send, Sync) erfordern, dass der Implementor Invarianten einhält, die der Compiler nicht verifizieren kann. Send bedeutet, dass ein Typ sicher zwischen Threads bewegt werden kann; Sync bedeutet, dass &T zwischen Threads geteilt werden kann. Der Compiler leitet diese für die meisten Typen automatisch ab, aber Raw Pointers sind standardmäßig nicht Send/Sync. Wenn du sie manuell implementierst, übernimmst du die Verantwortung für Thread-Sicherheit. static mut erfordert unsafe-Zugriff, weil mehrere Threads darum rennen könnten – bevorzuge Atomics (AtomicU64) oder Mutex. Dokumentiere immer die Safety-Rationale mit einem SAFETY-Kommentar.

rust
// Some traits are unsafe to implement — the compiler can't verify invariants
use std::marker::Send;

// Send/Sync are auto-implemented, but sometimes you must manually impl
struct RawPointer<T>(*mut T);

// SAFETY: We guarantee the pointer is only used on one thread
unsafe impl<T> Send for RawPointer<T> where T: Send {}

// Splitting a slice into disjoint mutable parts
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left = [1,2,3], right = [4,5,6] — disjoint, but compiler
// couldn't prove this without unsafe internally

// Global mutable state
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; }  // unsafe: data races possible

Unions & Inline-Assembly

Unions erlauben verschiedenen Typen, denselben Speicherort zu teilen – ein Feld zu lesen, das nicht zuletzt geschrieben wurde, ist undefiniertes Verhalten, daher unsafe. Sie sind hauptsächlich für FFI mit C. transmute reinterpret-castet das Bit-Pattern eines Typs in einen anderen derselben Größe – extrem gefährlich, wenn Größen differieren oder Typen inkompatibel sind. Inline-Assembly (asm!) lässt dich CPU-Instruktionen direkt einbetten, nützlich für Kernel-Entwicklung und extreme Optimierung. All dies sind scharfe Werkzeuge: verwende sie nur, wenn keine sichere Alternative existiert, und kapsle sie hinter einer sicheren Abstraktion.

rust
// Unions: multiple fields share the same memory (like C unions)
#[repr(C)]
union IntOrFloat {
    i: i32,
    f: f32,
}

let mut u = IntOrFloat { i: 42 };
unsafe { println!("as int: {}", u.i); }
u.f = 3.14;
unsafe { println!("as float: {}", u.f); }
// Reading the WRONG field is undefined behavior!

// Inline assembly (nightly / asm!)
#[cfg(feature = "asm")]
unsafe fn halt() {
    std::arch::asm!("hlt", options(nostack));
}

// Transmute: reinterpret bits as another type (same size)
let bits: u32 = 0x40490FDB;  // ~3.14159 in IEEE 754
let pi: f32 = unsafe { std::mem::transmute(bits) };
16

Iteratoren Deep Dive

Iterator-Trait & Erstellung

Das Iterator-Trait erfordert nur eine next()-Methode, die Option<Item> zurückgibt – None signalisiert Erschöpfung. Alles andere (map, filter, collect) baut darauf auf. iter() leiht Elemente (&T), into_iter() konsumiert die Collection (yielded owned T), iter_mut() yielded &mut T. Ranges (1..5, 1..=5) sind direkt Iteratoren. Strings iterieren nach chars (Unicode-Skalarwerte) oder Bytes. Iteratoren sind lazy – nichts läuft, bis du sie konsumierst.

rust
// The Iterator trait: one required method
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... many provided methods (map, filter, etc.)
}

// Creating iterators
let v = vec![1, 2, 3];
let iter = v.iter();        // borrows: &i32
let into_iter = v.into_iter(); // owns: i32 (consumes v)
let mut_iter = v.iter_mut();   // mutably borrows: &mut i32

// Range is an iterator
for i in 1..=5 { print!("{} ", i); }  // 1 2 3 4 5

// String iteration
for c in "héllo".chars() { print!("{} ", c); }  // h é l l o
for b in "hi".bytes() { print!("{} ", b); }      // 104 105

Adapter-Methoden (Lazy)

Adapter-Methoden transformieren Iteratoren und geben neue Iteratoren zurück – sie sind lazy, also erstellt das Verketten von map().filter().map() null intermediäre Collections. map wendet eine Funktion auf jedes Element an. filter behält Elemente, bei denen das Prädikat true zurückgibt. take(n) stoppt nach n Elementen (nützlich für unendliche Iteratoren). skip(n) verwirft die ersten n. flat_map mappt und flacht verschachtelte Iteratoren ab. enumerate paart jedes Element mit seinem Index. Nichts führt aus, bis ein Consumer (collect, sum, for-Schleife) den Iterator antreibt.

rust
let nums = vec![1, 2, 3, 4, 5, 6];

// map: transform each element
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();

// filter: keep elements matching predicate
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// take / skip: limit or skip elements
let first3: Vec<_> = nums.iter().take(3).collect();     // [1,2,3]
let after2: Vec<_> = nums.iter().skip(2).collect();     // [3,4,5,6]

// flat_map: map then flatten one level
let words: Vec<_> = ["a b", "c"].iter()
    .flat_map(|s| s.split(' '))
    .collect();  // ["a","b","c"]

// enumerate: add index
for (i, v) in nums.iter().enumerate() {
    println!("{}: {}", i, v);
}

Consumer-Methoden

Consumer-Methoden treiben die lazy-Iterator-Kette zur tatsächlichen Ausführung an. collect() sammelt Ergebnisse in jede Collection, die FromIterator implementiert (Vec, HashMap, String usw.). sum/product/count/fold reduzieren den Iterator auf einen einzelnen Wert. find/any/all short-circuit – sie stoppen, sobald die Antwort bekannt ist, also sind sie effizient bei unendlichen Iteratoren. min/max geben Option (None für leere Iteratoren). Die Kombination aus lazy-Adaptern + einem finalen Consumer bedeutet, dass Iterator-Ketten nach Optimierung so effizient sind wie handgeschriebene Schleifen.

rust
let nums = vec![1, 2, 3, 4, 5];

// collect: gather into a collection
let v: Vec<i32> = nums.iter().copied().collect();
let s: std::collections::HashSet<i32> = nums.iter().copied().collect();

// sum, product, count
let total: i32 = nums.iter().sum();        // 15
let prod: i32 = nums.iter().product();     // 120
let count = nums.iter().count();           // 5

// reduce / fold: accumulate into a single value
let max = nums.iter().copied().reduce(i32::max);  // Some(5)
let sum = nums.iter().fold(0, |acc, &x| acc + x); // 15

// find / any / all: short-circuiting
let first_even = nums.iter().find(|&&x| x % 2 == 0);  // Some(2)
let has_neg = nums.iter().any(|&x| *x < 0);  // false
let all_pos = nums.iter().all(|&x| *x > 0);  // true

// min / max
nums.iter().copied().min();  // Some(1)
nums.iter().copied().max();  // Some(5)

Benutzerdefinierte Iteratoren

Um einen benutzerdefinierten Iterator zu erstellen, implementiere das Iterator-Trait mit einer next()-Methode. Sobald du das tust, bekommst du alle 70+ Adapter- und Consumer-Methoden kostenlos. Der Iterator sollte seinen eigenen Zustand verfolgen (aktuelle Position usw.) und None zurückgeben, wenn erschöpft. Das Implementieren von IntoIterator für deinen Collection-Typ aktiviert for-Schleifen-Syntax. Für bidirektionalen oder wahlfreien Zugriff implementiere auch DoubleEndedIterator oder ExactSizeIterator. So stellen Vec, HashMap, Range und alle Standard-Collections Iteration bereit.

rust
// Build a custom iterator for a counter
struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Self {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let val = self.current;
            self.current += 1;
            Some(val)
        } else {
            None
        }
    }
}

// Now all iterator methods work!
let sum: usize = Counter::new(5).sum();  // 0+1+2+3+4 = 10
let doubled: Vec<_> = Counter::new(3).map(|x| x * 10).collect();

Unendliche & verkettete Iteratoren

Rust-Iteratoren können unendlich sein – (1..) generiert für immer natürliche Zahlen, repeat(x) wiederholt endlos. Diese sind sicher, weil Adapter lazy sind: take(n) begrenzt den Konsum. cycle() wiederholt einen endlichen Iterator unendlich. chain() verkettet Iteratoren sequenziell. zip() paart Elemente positional (stoppt beim kürzeren). peekable() lässt dich das nächste Element ansehen, ohne es zu konsumieren – nützlich für Parser. Das lazy-Design bedeutet, dass unendliche Iteratoren nichts kosten, bis sie konsumiert werden, und der Compiler Ketten zu engen Schleifen optimiert.

rust
use std::iter;

// Infinite iterators (use with take!)
let naturals = (1..).take(10);  // 1..10
let zeros = iter::repeat(0).take(5);  // [0,0,0,0,0]
let alternating = iter::repeat_with(|| rand::random::<u8>());

// cycle: repeat a finite iterator infinitely
let pattern = [1, 2, 3].iter().cycle().take(7);
// [1,2,3,1,2,3,1]

// chain: concatenate two iterators
let combined = [1, 2].iter().chain([3, 4].iter());
// [1,2,3,4]

// zip: pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter().zip(['a', 'b', 'c']).collect();
// [(1,'a'), (2,'b'), (3,'c')]

// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
if let Some(&&first) = iter.peek() { println!("{}", first); }
17

Fehlerbehandlung Deep Dive

Result- & Option-Kombinatoren

Kombinatoren lassen dich fehlgeschlagene Operationen ohne verschachtelte match-Ausdrücke verketten. map transformiert den Ok-Wert, map_err transformiert den Error. and_then verkettet Operationen, die selbst Result zurückgeben (flatmap für Errors). ok_or konvertiert Option→Result. unwrap_or/unwrap_or_else/unwrap_or_default bieten Fallback-Werte. Diese komponieren elegant: parse().map().and_then().map_err() erstellt eine Pipeline, wo jeder Schritt fehlschlagen kann und der erste Fehlschlag short-circuit. Bevorzuge Kombinatoren über unwrap() in Produktionscode.

rust
// Result combinators chain operations without match
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>().map(|n| n * 2)
}

// and_then: chain fallible operations
fn validate(n: i32) -> Result<i32, String> {
    if n > 0 { Ok(n) } else { Err("must be positive".into()) }
}
let result = "5".parse::<i32>().and_then(validate);  // Ok(5)

// map_err: transform the error type
let r = "abc".parse::<i32>()
    .map_err(|e| format!("Parse failed: {}", e));

// ok_or / ok_or_else: convert Option to Result
let opt: Option<i32> = None;
let val = opt.ok_or("missing value");  // Err("missing value")

// unwrap_or / unwrap_or_else / unwrap_or_default
let x: i32 = "abc".parse().unwrap_or(0);

Benutzerdefinierte Error-Typen

Benutzerdefinierte Error-Typen lassen dich domänenspezifische Fehlschläge repräsentieren. Das Schlüssel-Pattern: Implementiere From für jeden zugrundeliegenden Error-Typ, sodass der ?-Operator auto-konvertiert. Das bedeutet, du kannst ? mit std::io::Error, ParseIntError usw. ohne explizites map_err verwenden. Display zu implementieren macht den Error benutzerfreundlich; Debug ist für Entwickler. Enum-basierte Errors sind idiomatisch in Rust – sie sind erschöpfend (der Compiler warnt vor fehlenden Cases) und Zero-Cost (keine Heap-Allokation). Das ist die Grundlage vor der Verwendung von thiserror.

rust
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
    Unauthorized,
}

// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(s) => write!(f, "Not found: {}", s),
            AppError::Unauthorized => write!(f, "Unauthorized"),
        }
    }
}

// Now ? works for io::Error and ParseIntError automatically
fn read_config() -> Result<i32, AppError> {
    let s = std::fs::read_to_string("config.txt")?;  // io → AppError
    let n: i32 = s.trim().parse()?;                   // parse → AppError
    Ok(n)
}

Das Error-Trait & Box<dyn Error>

std::error::Error ist das Standardbibliotheks-Trait für Error-Typen (erfordert Debug + Display). Box<dyn Error> ist der einfachste Error-Typ – er akzeptiert jeden Error via ? und ist großartig für Prototyping oder Anwendungen, wo du keine spezifischen Errors programmatisch behandeln musst. Der Nachteil: Du verlierst den konkreten Error-Typ, also erfordert das Matchen auf spezifische Varianten downcast_ref. Für Bibliotheken bevorzuge einen konkreten Enum-Error-Typ (mit thiserror). Für Anwendungen ist anyhow eine bessere Wahl als Box<dyn Error>, weil es Backtraces und Error-Ketten bewahrt.

rust
use std::error::Error;

// std::error::Error is the trait for all errors
// Requires: Debug + Display
fn do_something() -> Result<(), Box<dyn Error>> {
    let f = std::fs::read_to_string("file.txt")?;  // io::Error
    let n: i32 = f.parse()?;                         // ParseIntError
    println!("{}", n);
    Ok(())
}

// Box<dyn Error> is the quick-and-dirty error type
// — accepts any error via ?, but loses specific type info

// downcast to recover specific error type
match do_something() {
    Err(e) => {
        if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
            println!("IO: {}", io_err);
        }
    }
    _ => {}
}

thiserror-Crate (Bibliotheks-Errors)

thiserror ist die Standard-Crate für Bibliotheks-Error-Typen. Das #[derive(Error)]-Makro generiert Display (aus #[error("...")]) und From (aus #[from]) automatisch. #[from] lässt ? den zugrundeliegenden Error in deine Enum-Variante konvertieren. Das eliminiert Boilerplate, während ein stark typisiertes, erschöpfendes Error-Enum erhalten bleibt. Verwende thiserror für Bibliotheken (wo Aufrufer auf spezifische Errors matchen müssen). Der {0}-Platzhalter fügt die Display des inneren Errors ein; benannte Felder wie {id} fügen Struct-Felder ein.

rust
use thiserror::Error;

// thiserror auto-generates Display and From impls
#[derive(Debug, Error)]
enum DataError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("parse failed: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("item {id} not found")]
    NotFound { id: u32 },

    #[error("invalid state: {msg}")]
    Invalid { msg: String },
}

// #[from] auto-implements From, so ? just works:
fn load() -> Result<i32, DataError> {
    let s = std::fs::read_to_string("data.txt")?;  // auto-converts
    let n: i32 = s.trim().parse()?;
    Ok(n)
}

anyhow-Crate (Anwendungs-Errors)

anyhow ist die Standard-Crate für Anwendungs-/Binary-Fehlerbehandlung. anyhow::Error wrappt jeden Error, der std::error::Error implementiert, und fügt Kontext, Backtraces und Error-Chaining hinzu. context() hängt eine menschenlesbare Nachricht an jeden fehlgeschlagenen Schritt an und erstellt eine Kette wie 'Failed to read config: IO error: No such file'. Das macht Debugging viel einfacher – du siehst genau, welcher Schritt fehlgeschlagen ist und warum. Verwende anyhow für main() und Anwendungscode, wo du nur Errors melden musst, nicht auf ihnen matchen. Verwende thiserror für Bibliotheken, wo Aufrufer typisierte Errors brauchen.

rust
use anyhow::{Context, Result, anyhow};

// anyhow::Result = Result<T, anyhow::Error>
fn load_config() -> Result<String> {
    let content = std::fs::read_to_string("config.toml")
        .context("Failed to read config.toml")?;  // add context
    Ok(content)
}

fn main() -> Result<()> {
    let config = load_config()?;
    if config.is_empty() {
        // bail! / anyhow! create errors with format! syntax
        return Err(anyhow!("config is empty"));
    }
    println!("{}", config);
    Ok(())
}

// Error chain: "Failed to read config.toml: No such file..."
// anyhow preserves the full chain and backtrace
18

Cargo & Crates Deep Dive

Cargo.toml-Struktur

Cargo.toml ist das Manifest für ein Rust-Projekt. [package] beschreibt die Crate-Metadaten. [dependencies] listet externe Crates – Versions-Strings verwenden semver (^1.0 bedeutet >=1.0, <2.0). features aktivieren optionale Funktionalität (serdes derive-Feature schaltet #[derive(Serialize)] ein). [dev-dependencies] sind nur für Tests/Benchmarks. [features] definieren Flags für bedingte Kompilierung. [profile.release] kontrolliert Optimierungs-Einstellungen. Das edition-Feld (2015/2018/2021) bestimmt Sprach-Features – verwende immer die neueste.

rust
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
description = "A sample app"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
rand = "0.8"

[dev-dependencies]
criterion = "0.5"  # only for tests/benches

[features]
default = ["tokio"]
async-mode = ["dep:tokio"]

[[bin]]
name = "myapp"
path = "src/main.rs"

[profile.release]
opt-level = 3
lto = true
strip = true

Abhängigkeiten & Feature-Flags

Feature-Flags aktivieren bedingte Kompilierung. Jede Abhängigkeit kann Features freigeben (z.B. serdes derive). default-features = false entfernt Default-Features, um die Binary-Größe zu reduzieren. Optionale Abhängigkeiten (optional = true) werden nur kompiliert, wenn ein Feature sie via dep:name aktiviert. Features sind additiv – sie schalten Dinge ein, nie aus. Das stellt Feature-Unification sicher: Wenn zwei Abhängigkeiten verschiedene Features von serde aktivieren, kompiliert Cargo serde einmal mit der Union aller Features. Verwende #[cfg(feature = "x")], um Code bedingt zu kompilieren.

rust
# Cargo.toml
[dependencies]
# Version requirements: ^1.2 (compatible), =1.2.3 (exact), >=1.0,<2.0 (range)
serde = "1.0"                    # ^1.0 (default: compatible)
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false }  # disable defaults

# Optional dependencies (enabled by a feature)
tokio = { version = "1", optional = true }

[features]
# "async" feature enables the optional tokio dep
async = ["dep:tokio"]
# Features can enable other features
full = ["async", "serde/derive"]

# In code:
# #[cfg(feature = "async")]
# fn run_async() { ... }

Workspaces (Multi-Crate-Projekte)

Workspaces gruppieren mehrere verwandte Crates, die sich ein Cargo.lock und Target-Verzeichnis teilen. Das beschleunigt Builds (geteilter Compile-Cache) und stellt sicher, dass alle Crates dieselben Abhängigkeits-Versionen verwenden. Member können über path = "../core" voneinander abhängen. [workspace.dependencies] zentralisiert Versionsverwaltung – Member-Crates referenzieren sie mit { workspace = true }. Der resolver = "2" (Standard in edition 2021) verwendet Feature-Unification pro-Target und vermeidet einige Build-Probleme. Verwende Workspaces für Monorepos, Bibliotheken mit mehreren Komponenten oder Projekte, die Core/CLI/Server aufteilen.

rust
# Root Cargo.toml — workspace manifest
[workspace]
members = [
    "core",
    "cli",
    "server",
    "utils",
]
resolver = "2"

# Shared dependencies across all members
[workspace.dependencies]
serde = "1.0"
tokio = "1"

# In member crates (e.g., cli/Cargo.toml):
# [dependencies]
# serde = { workspace = true }
# core = { path = "../core" }  # local path dependency

Build-Profile & Optimierung

Profile kontrollieren, wie cargo dein Projekt baut. dev (Standard für cargo build) priorisiert Compile-Geschwindigkeit. release (cargo build --release) priorisiert Runtime-Performance. Wichtige Knöpfe: opt-level (0-3, 's' für Größe, 'z' für minimale Größe), lto (Link-Time-Optimization über Crate-Grenzen), codegen-units (1 = beste Optimierung, aber langsamster Compile), strip (Symbole entfernen für kleinere Binaries), panic = 'abort' (deaktiviert Unwinding, kleinere Binary). Für Produktion verwende lto = true, codegen-units = 1, strip = true. Benutzerdefinierte Profile erben von existierenden.

rust
# Cargo.toml
[profile.dev]
opt-level = 0        # no optimization (fast compile)
debug = true         # include debug symbols
overflow-checks = true

[profile.release]
opt-level = 3        # max optimization
lto = "fat"          # link-time optimization across crates
codegen-units = 1    # single codegen unit (better opt, slower compile)
strip = true         # strip debug symbols from binary
panic = "abort"      # smaller binary, no unwinding

[profile.release.package."*"]
opt-level = 2  # optimize dependencies less than your code

# Custom profile
[profile.bench]
inherits = "release"
debug = true  # keep symbols for profiling

# Usage: cargo build --release --profile bench

Veröffentlichen & Dokumentation

Das Veröffentlichen auf crates.io ist permanent – Versionen können nicht überschrieben oder gelöscht werden (nur yanked, was neue Abhängige verhindert). Stelle sicher, dass name, version, description, license und repository gesetzt sind. cargo package validiert das Manifest und zeigt, was veröffentlicht würde. cargo doc generiert HTML-Dokumentation aus ///-Doc-Kommentaren – Doc-Tests (Code in ```-Blöcken) werden von cargo test kompiliert und ausgeführt. Gute Doc-Kommentare mit Beispielen sind sowohl Dokumentation als auch Tests. Verwende #[doc(hidden)], um interne Items zu verbergen.

rust
# Before publishing:
# 1. Login (one-time)
# $ cargo login <token>  (token from crates.io)

# 2. Check the package
# $ cargo package        # creates .crate file, checks metadata
# $ cargo publish --dry-run

# 3. Publish
# $ cargo publish        # uploads to crates.io (irreversible!)

# Required fields in Cargo.toml for publishing:
# name, version, description, license, repository

# Documentation:
# $ cargo doc            # generate docs for your crate + deps
# $ cargo doc --open     # generate and open in browser
# $ cargo doc --no-deps  # only your crate

# Doc tests run automatically:
/// Adds two numbers.
/// 
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
19

Trait-Objekte & Dynamischer Dispatch

dyn Trait-Grundlagen

Trait-Objekte (dyn Trait) ermöglichen Runtime-Polymorphismus: Eine einzelne Variable kann verschiedene konkrete Typen halten, die denselben Trait implementieren. Der Compiler generiert eine Vtable (Virtual Method Table) pro Typ, und Methodenaufrufe gehen über die Vtable (dynamischer Dispatch). Das hat kleine Runtime-Kosten, ermöglicht aber heterogene Collections. Verwende Trait-Objekte, wenn der konkrete Typ zur Compile-Zeit unbekannt oder variabel ist.

rust
trait Draw {
    fn draw(&self);
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Draw for Circle {
    fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Square {
    fn draw(&self) { println!("Square s={}", self.side); }
}

// Trait object: type-erased, dynamic dispatch
let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }

// Function taking trait object
fn render(shape: &dyn Draw) { shape.draw(); }

Object-Safety

Ein Trait ist nur dann Object-safe (kann als dyn Trait verwendet werden), wenn: er keine Methoden hat, die Self zurückgeben, keine Methoden, die Self by Value nehmen, keine generischen Methoden und alle Methoden dispatchable sind. Clone, Default und From sind NICHT Object-safe. Workarounds umfassen die Verwendung von Box<Self>-Returns (box_clone-Pattern), das Aufteilen von Traits oder die Verwendung von statischem Dispatch mit Enums. Der Compiler meldet Object-Safety-Verletzungen klar.

rust
// Object-safe trait (can be made into dyn)
trait Animal {
    fn sound(&self) -> String;
    fn name(&self) -> &str;
}

// NOT object-safe: returns Self
trait Clone {
    fn clone(&self) -> Self;  // Self is unknown for dyn
}

// NOT object-safe: takes Self by value
trait Add {
    fn add(&self, other: Self) -> Self;
}

// NOT object-safe: generic method
trait From {
    fn from<T>(t: T) -> Self;
}

// Fix: use where clauses or separate traits
trait AnimalSafe {
    fn sound(&self) -> String;
    fn box_clone(&self) -> Box<dyn AnimalSafe>;
}

Statischer vs Dynamischer Dispatch

Statischer Dispatch (Generics mit Trait-Bounds) monomorphisiert: Der Compiler generiert eine spezialisierte Version pro konkretem Typ, was Inlining und maximale Performance auf Kosten der Binary-Größe ermöglicht. Dynamischer Dispatch (dyn Trait) verwendet Vtable-Lookups zur Runtime, kleinere Binary, aber langsamere Aufrufe (verhindert Inlining). Bevorzuge statischen Dispatch für Performance-kritischen Code; verwende dynamischen Dispatch für heterogene Collections und Plugin-Systeme.

rust
// Static dispatch (monomorphization)
fn max<T: Ord>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
// Compiler generates max_i32, max_f64, etc.

// Dynamic dispatch (vtable lookup)
fn max_dyn(a: &dyn Ord, b: &dyn Ord) -> bool {
    // a > b  // Cannot use operators on dyn
    false
}

// Trait bound (static)
fn process<T: Display>(item: &T) {
    println!("{}", item);
}

// impl Trait (static, syntactic sugar)
fn process2(item: &impl Display) {
    println!("{}", item);
}

// dyn Trait (dynamic)
fn process3(item: &dyn Display) {
    println!("{}", item);
}

Trait-Objekte mit Lifetimes

Trait-Objekte können Lifetime-Bounds tragen: Box<dyn Trait + 'a> bedeutet, dass das Trait-Objekt (und der konkrete Typ dahinter) mindestens 'a leben muss. Standardmäßig impliziert Box<dyn Trait> 'static. Wenn du Trait-Objekte speicherst, die Referenzen halten könnten, füge die Lifetime explizit hinzu. Die +-Syntax kombiniert Trait-Bounds mit Lifetimes. Häufig in Plugin-Systemen und Event-Handlern.

rust
trait Parser {
    fn parse(&self, input: &str) -> &str;
}

// Trait object with lifetime
fn make_parser() -> Box<dyn Parser> {
    Box::new(MyParser)
}

// Trait object holding references
struct Runner<'a> {
    parsers: Vec<Box<dyn Parser + 'a>>,
}

impl<'a> Runner<'a> {
    fn add(&mut self, p: Box<dyn Parser + 'a>) {
        self.parsers.push(p);
    }
}

// dyn Trait defaults to 'static when no lifetime given
fn static_parser() -> Box<dyn Parser> {
    Box::new(MyParser)
}

Downcasting & Any

Der Any-Trait ermöglicht Runtime-Typ-Prüfung und Downcasting. Any wird automatisch für alle 'static-Typen implementiert. downcast_ref und downcast_mut geben Option zurück und erlauben sichere Typ-Wiederherstellung. Nützlich für Plugin-Systeme, dynamische Konfigurationen und heterogene Container. Verwende Any sparsam – es umgeht das Typsystem. Bevorzuge Enums für bekannte Alternativen und Generics für typsicheren Polymorphismus.

rust
use std::any::Any;

// Any enables runtime type identification
let x: Box<dyn Any> = Box::new(42_i32);

// Downcast to concrete type
if let Some(n) = x.downcast_ref::<i32>() {
    println!("Got i32: {}", n);
}

// Store heterogeneous values
let mut bag: Vec<Box<dyn Any>> = vec![
    Box::new(42_i32),
    Box::new("hello".to_string()),
    Box::new(3.14_f64),
];

for item in &bag {
    if let Some(s) = item.downcast_ref::<String>() {
        println!("String: {}", s);
    } else if let Some(n) = item.downcast_ref::<i32>() {
        println!("i32: {}", n);
    }
}
20

Deklarative Makros

macro_rules!-Grundlagen

macro_rules! definiert deklarative Makros, die Patterns matchen und zu Code expandieren. $( $x:expr ),* matcht eine komma-getrennte Liste von Ausdrücken, null oder mehrmals wiederholt. Der $() ... *-Block wird für jeden Match wiederholt. Makros werden zur Compile-Zeit vor der Typprüfung expandiert. Sie sind hygienisch: Identifiers, die vom Makro eingeführt werden, kollidieren nicht mit umgebendem Code. Verwende Makros, um Boilerplate zu reduzieren (vec!, println!, format!).

rust
macro_rules! vec_of {
    ( $( $x:expr ),* ) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

let nums = vec_of!(1, 2, 3, 4);
let strs = vec_of!("a", "b", "c");

// Multiple patterns
macro_rules! greet {
    () => { println!("Hello!") };
    ($name:expr) => { println!("Hello, {}!", $name) };
    ($name:expr, $greeting:expr) => {
        println!("{}, {}!", $greeting, $name)
    };
}
greet!();
greet!("Alice");
greet!("Bob", "Hi");

Fragment-Typen

Makro-Fragmente haben spezifische Typen: expr (Ausdrücke), stmt (Statements), ty (Typen), pat (Patterns), ident (Identifiers), tt (Token-Bäume, die flexibelsten), literal (Literale) und mehr. Der Fragment-Typ bestimmt, was das Makro akzeptiert und wie es parst. tt ist der allgemeinste – jede gültige Token-Sequenz. Verwende den spezifischsten Typ, der möglich ist, für bessere Fehlermeldungen. Der Parser folgt der Most-Recently-Added-Ambiguity-Regel.

rust
macro_rules! items {
    // $x:expr - expression (1 + 2, foo())
    // $x:stmt - statement (let x = 5;)
    // $x:ty - type (Vec<i32>, &str)
    // $x:pat - pattern (Some(x), (a, b))
    // $x:path - path (std::vec::Vec, Module::Type)
    // $x:ident - identifier (foo, Bar)
    // $x:literal - literal (42, "hello")
    // $x:tt - token tree (anything)
    // $x:block - block ({ ... })
    // $x:item - item (fn, struct)

    ($e:expr, $t:ty, $i:ident) => {
        let $i: $t = $e;
    };
}

items!(42, i32, my_num);
items!("hi", &str, greeting);

Repetitions-Patterns

Repetition in Makros: $(...)* matcht null oder mehr, $(...)+ matcht eins oder mehr, $(...)? matcht null oder eins. Separatoren wie Kommas gehen zwischen Matches. Verschachtelte Repetitions behandeln mehrdimensionale Daten (Matrizen, Listen von Listen). Der $() ... *-Expansions-Block wiederholt für jeden Match. Mehrere Variablen in derselben Repetition müssen gleich oft matchen. Verwende @prefix-interne Regeln für Akkumulator-Patterns.

rust
macro_rules! sum {
    // Zero or more: $(...)*
    ( $( $x:expr ),* ) => {
        {
            let mut total = 0;
            $(
                total += $x;
            )*
            total
        }
    };

    // One or more: $(...)+
    ( first $(, $x:expr )+ ) => {
        println!("First and more");
    };

    // With separator and count
    ( $( $x:expr ),+ $(; $sep:expr )? ) => {
        println!("List with optional separator");
    };
}

// Nested repetition
macro_rules! matrix {
    ( $( [ $( $x:expr ),* ] ),* ) => {
        vec![ vec![ $( $x ),* ],* ]
    };
}

Hygiene & Exportieren

Makro-Hygiene verhindert Identifier-Kollisionen: Variablen, die von einem Makro eingeführt werden, leben in ihrem eigenen Scope und fangen oder shadowen keine Aufrufer-Variablen. Verwende $crate, um auf Items in der Crate zu referenzieren, in der das Makro definiert ist, was sicherstellt, dass es nach Re-Export funktioniert. Die @prefix-Konvention markiert interne Helfer-Regeln, die Benutzer nicht direkt aufrufen sollten. #[macro_export] veröffentlicht das Makro am Crate-Root.

rust
// Export at crate root
#[macro_export]
macro_rules! log {
    ($($arg:tt)*) => {
        // Hygienic: this T does not collide with caller's T
        let _ts: std::time::Instant = std::time::Instant::now();
        eprintln!("[{}] {}", "LOG", format!($($arg)*));
    };
}

// Use $crate to refer to crate items (works after re-export)
#[macro_export]
macro_rules! make_error {
    ($msg:expr) => {
        $crate::Error::new($msg)
    };
}

// Internal helper rule (convention: @ prefix)
macro_rules! count {
    (@count $x:expr, $($rest:expr),*) => { 1 + count!(@count $($rest),*) };
    (@count $x:expr) => { 1 };
    () => { 0 };
}

Häufige Makro-Patterns

Makros glänzen beim Bauen von DSLs und Reduzieren von Boilerplate. Häufige Patterns: Builder-DSLs (html!, sql!), Test-Assertions (assert_approx!), Konfiguration (config!) und Code-Generierung (Derive-ähnliche Makros). Makros sind hygienisch und Compile-Zeit, also haben sie keine Runtime-Kosten. Einschränkungen: keine Rekursionstiefe über 64, komplexe Fehlermeldungen und Schwierigkeit mit nicht-trivialem Parsen. Für komplexes Metaprogramming verwende prozedurale Makros.

rust
// 1. Builder DSL
macro_rules! html {
    ($tag:ident { $($body:tt)* }) => {
        format!("<{}>{}</{}>", stringify!($tag), html_inner!($($body)*), stringify!($tag))
    };
}

// 2. Test assertion
macro_rules! assert_approx {
    ($a:expr, $b:expr, $eps:expr) => {
        assert!(($a - $b).abs() < $eps, "{} != {} within {}", $a, $b, $eps);
    };
}

// 3. Configuration
macro_rules! config {
    ( $( $key:ident : $val:expr ),* ) => {
        {
            let mut m = std::collections::HashMap::new();
            $(
                m.insert(stringify!($key).to_string(), $val.to_string());
            )*
            m
        }
    };
}

let cfg = config! { host: "localhost", port: 8080 };
21

Cargo & Workspaces

Workspace-Setup

Workspaces gruppieren mehrere Crates, die sich Abhängigkeiten und ein Target-Verzeichnis teilen. Member werden explizit oder über Globs aufgelistet. [workspace.package] definiert geteilte Paket-Metadaten, geerbt mit .workspace = true. [workspace.dependencies] zentralisiert Abhängigkeits-Versionen und stellt sicher, dass alle Crates dieselbe Version verwenden. Das verhindert Versionskonflikte und beschleunigt Builds (einzelnes Cargo.lock). Verwende Workspaces für Multi-Crate-Projekte wie CLI + Bibliothek + Server.

rust
# Root Cargo.toml
[workspace]
members = ["crates/*", "cli", "server"]
resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <[email protected]>"]
license = "MIT"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"

# Member crate: crates/mylib/Cargo.toml
[package]
name = "mylib"
version.workspace = true
edition.workspace = true

[dependencies]
serde.workspace = true
tokio.workspace = true

Build-Profile

Profile kontrollieren Compile-Einstellungen. dev priorisiert schnelle Kompilierung (opt-level 0, Debug-Symbole). release maximiert Runtime-Performance (opt-level 3, LTO, einzelne Codegen-Unit). LTO (Link-Time-Optimization) ermöglicht Cross-Crate-Inlining. panic = "abort" produziert kleinere Binaries, deaktiviert aber Unwinding. Per-Package-Overrides (profile.dev.package."*") optimieren Abhängigkeiten sogar in dev. Benutzerdefinierte Profile erben von existierenden.

rust
# Cargo.toml
[profile.dev]
opt-level = 0        # No optimization (fast compile)
debug = true         # Include debug symbols
overflow-checks = true

[profile.release]
opt-level = 3        # Max optimization
debug = false
lto = "fat"          # Link-time optimization
codegen-units = 1    # Single unit (better opt, slower compile)
panic = "abort"      # Smaller binary, no unwinding
strip = true         # Strip symbols

[profile.dev.package."*"]
opt-level = 2        # Optimize dependencies in dev

# Custom profile
[profile.bench]
inherits = "release"
debug = true

# Usage: cargo build --release, cargo build --profile=bench

Features & Bedingte Kompilierung

Features aktivieren bedingte Kompilierung. Optionale Abhängigkeiten werden automatisch zu Features. cfg(feature = "...") gate-t Code nach Feature. Das Default-Feature-Set ist aktiviert, außer --no-default-features wird übergeben. Features sollten additiv sein (mehr aktivieren, nicht weniger). Verwende Features, um Binary-Größe zu reduzieren, mehrere Backends zu unterstützen oder experimentellen Code zu gaten. Kombiniere mit cfg_attr für bedingte Derive-Makros. Vermeide sich gegenseitig ausschließende Features.

rust
# Cargo.toml
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
async-runtime = ["tokio"]

[dependencies]
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }

# Code: conditional compilation
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
    serde_json::from_str(s)
}

#[cfg(not(feature = "json"))]
pub fn parse_json(_: &str) -> Result<Value, Error> {
    Err(Error::FeatureNotEnabled)
}

Build-Skripte (build.rs)

build.rs läuft vor der Kompilierung und ermöglicht Code-Generierung, Environment-Einbettung und C-Bibliotheks-Linking. cargo:rerun-if-*-Direktiven kontrollieren, wann das Skript neu läuft. Generiere Code mit println!-Makros und include! ihn dann in deine Crate. Häufige Verwendungen: Versions-Info einbetten, Bindings generieren (bindgen), protobuf-/SQL-Schemas kompilieren und System-Bibliotheken linken. Halte Build-Skripte schnell – sie laufen bei jedem Build.

rust
// build.rs: runs before compilation
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    // Tell Cargo to rerun if env changes
    println!("cargo:rerun-if-env-changed=DATABASE_URL");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("config.rs");

    let db_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite://default.db".to_string());

    // Generate Rust code at build time
    fs::write(&dest, format!(
        "pub const DATABASE_URL: &str = \"{}\";",
        db_url
    )).unwrap();

    // Link a C library
    println!("cargo:rustc-link-lib=static=mylib");
    println!("cargo:rustc-link-search=native=/usr/local/lib");
}

// In code: include generated file
include!(concat!(env!("OUT_DIR"), "/config.rs"));

Veröffentlichen & Dokumentation

cargo publish lädt eine Crate auf crates.io hoch. Verwende --dry-run zur Verifizierung vor dem Veröffentlichen. cargo doc generiert HTML-Dokumentation aus Doc-Kommentaren (///). Code-Blöcke in Doc-Kommentaren werden mit cargo test --doc getestet. Schließe Examples-, Panics- und Errors-Abschnitte ein. Metadaten (description, repository, keywords) verbessern Auffindbarkeit. Einmal veröffentlicht, kann eine Version nicht wiederverwendet oder gelöscht werden – verwende yank, um neue Projekte davon abzuhalten, sie zu verwenden.

rust
# Publish to crates.io
cargo login <token>     # One-time authentication
cargo publish           # Publish current crate

# Before publishing:
cargo publish --dry-run # Verify package contents
cargo package           # Inspect the .crate file

# Documentation
cargo doc               # Generate docs
cargo doc --open        # Generate and open
cargo doc --no-deps     # Only this crate

# In code: doc comments
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

# README and metadata in Cargo.toml
[package]
description = "A short description"
repository = "https://github.com/user/repo"
readme = "README.md"
keywords = ["parser", "cli"]
categories = ["command-line-utilities"]
22

Prozedurale Makros

Makro-Typen

Prozedurale Makros generieren Rust-Code zur Compile-Zeit und operieren auf Token-Streams. Drei Typen: funktionsähnlich (custom!()), Derive (#[derive(Custom)]) und Attribut (#[custom]). Sie erfordern eine separate Crate mit proc-macro = true. Die syn-Crate parst Rust-Syntax, quote generiert Code und proc-macro2 ermöglicht Testing. Proc-Makros sind mächtig, aber komplex – verwende sie für Derive-Makros, DSLs und Code-Generierung, die deklarative Makros nicht handhaben können.

rust
// Three types of procedural macros:
// 1. Function-like: my_macro!(...)
// 2. Derive: #[derive(MyMacro)]
// 3. Attribute: #[my_macro]

// Cargo.toml for a proc-macro crate
// [lib]
// proc-macro = true

// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> i32 { 42 }".parse().unwrap()
}

// Usage: make_answer!();
// Generates: fn answer() -> i32 { 42 }

Derive-Makro

Derive-Makros fügen Typen, die mit #[derive(MyMacro)] annotiert sind, Trait-Implementierungen hinzu. syn parst die Eingabe in einen DeriveInput-AST. quote! generiert Code mit #-Interpolation für Variablen. Helfer-Attribute (attributes(hello)) erlauben Anpassung an Feldern oder Varianten. Häufige Derive-Makros: Debug, Clone, Serialize, Deserialize. Der generierte Code wird an das Modul angehängt, also kann er den Originaltyp nicht modifizieren.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;

    let expanded = quote! {
        impl HelloMacro for #name {
            fn hello() {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };

    expanded.into()
}

// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
// Pancakes::hello();  // "Hello from Pancakes!"

// Helper attributes
#[proc_macro_derive(HelloMacro, attributes(hello))]
pub fn hello_with_attr(input: TokenStream) -> TokenStream { /* ... */ }

Attribut-Makro

Attribut-Makros (#[my_attr]) transformieren das Item, das sie annotieren, und können es potenziell vollständig ersetzen. Sie empfangen sowohl die Attribut-Argumente als auch das annotierte Item. Häufige Verwendungen: Logging, Caching, Async-Wrapper (#[tokio::main]) und Routing (#[get("/path")]). Attribut-Makros können die Signatur des Items ändern, Code hinzufügen oder zusätzliche Items generieren. Sie sind flexibler als Derive-Makros, aber schwerer korrekt zu verwenden.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
    let attr_args = syn::parse_macro_input!(attr as syn::AttributeArgs);
    let input_fn = parse_macro_input!(item as ItemFn);

    let fn_name = &input_fn.sig.ident;
    let fn_block = &input_fn.block;

    let expanded = quote! {
        fn #fn_name() {
            println!("Calling {}", stringify!(#fn_name));
            let __result = (|| #fn_block)();
            println!("Finished {}", stringify!(#fn_name));
            __result
        }
    };

    expanded.into()
}

// Usage:
// #[log_calls]
// fn my_function() -> i32 { 42 }

Funktionsähnliches Makro

Funktionsähnliche prozedurale Makros (my_macro!()) akzeptieren beliebige Token-Streams und ermöglichen benutzerdefinierte DSLs. Implementiere Parse, um akzeptierte Syntax zu definieren. Das Makro kann Eingabe validieren, transformieren oder Code basierend auf der Eingabe generieren. Häufige Verwendungen: SQL-Queries (sqlx), HTML-Templates (maud) und Konfigurations-DSLs. Anders als deklarative Makros können Proc-Makros komplexe Syntax parsen und beliebige Berechnungen zur Compile-Zeit durchführen. Halte sie schnell, um langsame Builds zu vermeiden.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input};

// Custom syntax: sql!(SELECT * FROM users WHERE id = $1)
struct SqlQuery {
    query: String,
}

impl Parse for SqlQuery {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let query = input.to_string();
        Ok(SqlQuery { query })
    }
}

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let SqlQuery { query } = parse_macro_input!(input as SqlQuery);

    let expanded = quote! {
        {
            static QUERY: &str = #query;
            // Compile-time SQL validation could go here
            QUERY
        }
    };

    expanded.into()
}

Testing & Debugging

Proc-Makros testen: trybuild führt UI-Tests aus, die Compiler-Ausgabe (Erfolgs- oder Fehlermeldungen) gegen erwartete Dateien vergleichen. Für Derive-Makros teste, dass der generierte Code kompiliert und sich korrekt verhält. Debugge mit eprintln! (während der Kompilierung gedruckt) oder cargo expand (zeigt expandierte Makro-Ausgabe). Makro-Entwicklung ist iterativ: Schreibe das Makro, verwende cargo expand zur Inspektion der Ausgabe, behebe Probleme. Dokumentiere die Syntax und unterstützte Features des Makros klar.

rust
// Cargo.toml
// [dev-dependencies]
// trybuild = "1"

// tests/ui/my_macro.rs - test file
// #[derive(MyMacro)]
// struct Foo;
// fn main() { Foo::hello(); }

// tests/ui/my_macro.stderr - expected error
// error: ...

// Test runner
#[test]
fn ui() {
    let t = trybuild::TestCases::new();
    t.pass("tests/ui/pass_*.rs");
    t.compile_fail("tests/ui/fail_*.rs");
}

// Debugging with eprintln
#[proc_macro_derive(Debug)]
pub fn debug_derive(input: TokenStream) -> TokenStream {
    eprintln!("Input tokens: {}", input);
    let ast = syn::parse2(input.clone().into()).unwrap();
    eprintln!("Parsed AST: {:#?}", ast);
    TokenStream::new()
}
23

Makros

macro_rules!

macro_rules! definiert deklarative Makros. $x ist ein Capture, expr matcht Ausdrücke. $(...)* wiederholt. Makros werden zur Compile-Zeit expandiert. Nützlich zum Reduzieren von Boilerplate. Das Standard-vec!-Makro funktioniert ähnlich.

rust
macro_rules! vec_of {
    ($($x:expr),*) => {{
        let mut v = Vec::new();
        $(v.push($x);)*
        v
    }};
}
let nums = vec_of!(1, 2, 3);

Prozedurale Makros

Prozedurale Makros generieren Code zur Compile-Zeit. Drei Typen: Derive (#[derive(Debug)]), Attribut (#[my_attr]), funktionsähnlich (my_macro!). Mächtiger als macro_rules!, erfordern aber eine separate Crate. Verwendet von serde, tokio und diesel.

rust
// In a separate crate with proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Generate impl HelloMacro for the type
    // Returns new TokenStream
}

Häufige Makros

Eingebaute Makros: println!/format! für Ausgabe, vec! für Vektoren, assert!/assert_eq! für Tests, dbg! für Debugging, todo!/unreachable! für Kontrollfluss. Alle sind macro_rules!-basiert. dbg! gibt den Wert für Chaining zurück.

rust
println!("Hello, {}!", "world");
format!("x = {}", 42);
vec![1, 2, 3];
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
dbg!(some_variable);  // Debug print
todo!("Not implemented");
unreachable!();

Makro-Hygiene

Rust-Makros sind hygienisch: Identifiers, die vom Makro eingeführt werden, kollidieren nicht mit Identifiers im aufrufenden Scope. Das verhindert subtile Bugs. Das temp innerhalb des Makros ist unterschiedlich vom äußeren temp. Deklarative Makros sind immer hygienisch.

rust
macro_rules! swap {
    ($a:expr, $b:expr) => {
        let temp = $a;
        $a = $b;
        $b = temp;
    };
}
// temp is hygienic: does not conflict with outer temp
let mut temp = 1;
let mut x = 2;
swap!(temp, x);  // Works correctly

Repetition

Repetition in Makros: $(...)* matcht null oder mehr, $(...)+ eins oder mehr. Der Separator (Komma) kann angegeben werden. $x erfasst jeden Wert. Nützlich für variadic-ähnliche Funktionen. Das Standard-println! verwendet dies für mehrere Argumente.

rust
macro_rules! sum {
    ($($x:expr),*) => {
        0 $(+ $x)*
    };
}
let total = sum!(1, 2, 3, 4);  // 10
// $(...)* zero or more, $(...)+ one or more
// $(...),? optional trailing comma
24

Async Deep Dive

async/await

async fn gibt ein Future zurück. .await suspendiert, bis das Future bereit ist. Futures sind lazy: Nichts läuft, bis sie awaited werden. Der Compiler transformiert async fn in eine Zustandsmaschine. Verwende die tokio- oder async-std-Runtime zur Ausführung.

rust
async fn fetch_data() -> String {
    // Simulate async work
    String::from("data")
}
async fn process() {
    let data = fetch_data().await;
    println!("{}", data);
}

Tokio-Runtime

tokio::main aktiviert async main. spawn erstellt einen Task (wie einen Green Thread). join! wartet auf mehrere Futures gleichzeitig. Tokio bietet I/O, Timer und Scheduling. Die beliebteste Async-Runtime in Rust.

rust
#[tokio::main]
async fn main() {
    let task1 = tokio::spawn(async { work1().await });
    let task2 = tokio::spawn(async { work2().await });
    let (r1, r2) = tokio::join!(task1, task2);
}

Channels (async)

mpsc (Multi-Producer, Single-Consumer)-Channels ermöglichen Async-Kommunikation. send/recv sind async. Der Channel hat einen Buffer (32 Nachrichten). Wenn alle Sender droppen, gibt recv None zurück. Nützlich für Producer-Consumer-Patterns.

rust
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);
    tokio::spawn(async move {
        tx.send("hello").await.unwrap();
    });
    while let Some(msg) = rx.recv().await {
        println!("{}", msg);
    }
}

Select

select! wartet auf das erste von mehreren Futures, das abgeschlossen wird. Andere Futures werden gedroppt. Nützlich für Timeouts und rennende Operationen. Das Pattern ist häufig in Netzwerk-Servern. Jeder Zweig kann ein Guard-Pattern haben.

rust
tokio::select! {
    result = task1 => {
        println!("Task1 done: {:?}", result);
    }
    result = task2 => {
        println!("Task2 done: {:?}", result);
    }
    _ = tokio::time::sleep(Duration::from_secs(5)) => {
        println!("Timeout");
    }
}

Stream

Stream ist das Async-Äquivalent von Iterator. next().await bekommt das nächste Item. StreamExt bietet map, filter, for_each. Nützlich für die Verarbeitung von Daten-Chunks aus Netzwerk oder Dateien. Die async-stream-Crate vereinfacht das Erstellen von Streams.

rust
use tokio_stream::{self as stream, StreamExt};
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(item) = stream.next().await {
    println!("{}", item);
}
// Map, filter like iterators but async
stream.map(|x| x * 2).filter(|x| *x > 2).for_each(|x| async move {
    println!("{}", x);
}).await;
25

Cargo & Crates

Cargo.toml

Cargo.toml ist die Manifest-Datei. [package] definiert Metadaten. [dependencies] listet externe Crates. Features aktivieren optionale Funktionalität. [dev-dependencies] sind nur für Tests. Edition 2021 ist die neueste stabile. Versionen verwenden semver.

rust
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
pretty_assertions = "1"

Cargo-Befehle

cargo new erstellt ein Binary-Projekt (--lib für Bibliothek). build kompiliert nach target/. --release aktiviert Optimierungen. check ist schneller als build (kein Codegen). clippy fängt häufige Fehler ab. fmt formatiert Code. doc generiert HTML-Dokumentation. add fügt eine Abhängigkeit ein.

rust
cargo new myapp        # Create new project
cargo build            # Compile
cargo build --release  # Optimized build
cargo run              # Build and run
cargo test             # Run tests
cargo check            # Fast type-check
cargo fmt              # Format code
cargo clippy           # Lint
cargo doc --open       # Generate docs
cargo add serde        # Add dependency

Workspaces

Workspaces gruppieren mehrere Crates, die sich ein Target-Verzeichnis und Cargo.lock teilen. Member sind einzelne Crates. [workspace.dependencies] zentralisiert Abhängigkeits-Versionen. Jede Crate referenziert sie mit workspace = true. Schnellere Builds durch geteilte Kompilierung. Verwendet von großen Projekten wie rust-analyzer.

rust
# Root Cargo.toml
[workspace]
members = ["crate-a", "crate-b"]

# Shared dependencies
[workspace.dependencies]
serde = "1.0"

# In crate-a/Cargo.toml
[dependencies]
serde = { workspace = true }

Features

Features aktivieren bedingte Kompilierung. Default-Features sind aktiviert, außer --no-default-features. cfg(feature = ...) gate-t Code. dep:-Syntax in Abhängigkeiten vermeidet Feature-Unification. Nützlich für optionale Funktionalität und plattformspezifischen Code. Crates können Features an Konsumenten freigeben.

rust
# Cargo.toml
[features]
default = ["csv"]
csv = ["dep:csv-parse"]
json = ["dep:serde_json"]

# Conditional compilation
#[cfg(feature = "csv")]
pub fn parse_csv() { /* ... */ }

Veröffentlichen

crates.io ist das Rust-Paket-Register. cargo login authentifiziert. --dry-run fängt Probleme ab. Einmal veröffentlicht, kann eine Version nicht erneut veröffentlicht werden (yank verbirgt nur vor der Suche). Folge semver: patch für Fixes, minor für Features, major für Breaking Changes. README und Lizenz sind erforderlich.

rust
# Login (one time)
cargo login <token>

# Check before publishing
cargo publish --dry-run

# Publish to crates.io
cargo publish

# Version bumping
cargo bump patch  # 0.1.0 -> 0.1.1
cargo bump minor  # 0.1.0 -> 0.2.0
cargo bump major  # 0.1.0 -> 1.0.0
26

Rust testen

Unit-Tests

Tests leben in einem #[cfg(test)]-Modul. use super::* importiert das Eltern-Modul. #[test] markiert Test-Funktionen. assert_eq! prüft Gleichheit. #[should_panic] erwartet einen Panic. Tests laufen mit cargo test. Unit-Tests sind bei Code platziert. Das cfg(test)-Attribut stellt sicher, dass Tests nicht in Release-Builds kompiliert werden.

rust
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
    #[test]
    #[should_panic]
    fn test_panic() {
        panic!("expected");
    }
}

Integration-Tests

Integration-Tests leben im tests/-Verzeichnis. Jede Datei wird als separate Crate kompiliert. Sie können nur die öffentliche API testen. Nützlich für End-to-End-Testing. Führe spezifische Tests aus mit --test <name>. Integration-Tests sind langsamer zu kompilieren, testen aber die echte Schnittstelle.

rust
// tests/integration_test.rs
use myapp::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(2, 3), 5);
}
// Run: cargo test --test integration_test
// Each file in tests/ is a separate crate

Test-Organisation

#[ignore] überspringt einen Test, außer --ignored wird übergeben. Filtere Tests nach Namen-Pattern. --nocapture zeigt println!-Ausgabe. Tests laufen standardmäßig parallel. Verwende --test-threads=1 für sequenziell. Benutzerdefinierte Harnesses können den Standard-Test-Runner ersetzen. Nützlich für Benchmarks und Property-Tests.

rust
#[test]
fn it_works() { /* ... */ }

// Custom test harness
#[test]
#[ignore = "slow test"]
fn slow_test() { /* ... */ }

// Run only ignored tests
cargo test -- --ignored

// Filter by name
cargo test test_add

// Show output
cargo test -- --nocapture

Assertions

assert! prüft einen Boolean. assert_eq!/assert_ne! vergleichen Werte mit Debug-Ausgabe bei Fehlschlag. Benutzerdefinierte Nachrichten helfen beim Debugging. Für Gleitkomma verwende die approx-Crate. Für partielle Gleichheit implementiere PartialEq. Die Debug-Ausgabe zeigt beide Werte, wenn die Assertion fehlschlägt.

rust
assert!(true);                          // Boolean
assert_eq!(2 + 2, 4);                   // Equality
assert_ne!(3, 4);                       // Inequality
assert!(x > 0, "x must be positive");   // Custom message
assert_eq!(a, b, "got {}, expected {}", a, b);
// Debug output on failure
assert_eq!(vec![1, 2], vec![1, 2]);

Property-Testing

proptest generiert zufällige Eingaben, um fehlschlagende Cases zu finden. Strategien (a in range) definieren Eingabe-Generatoren. prop_assert! meldet Fehlschläge mit minimalen Gegenbeispielen. Shrinking findet die kleinste fehlschlagende Eingabe. Besser als handgeschriebene Tests für Edge-Cases. Ähnlich wie QuickCheck in Haskell.

rust
// Cargo.toml: proptest = "1"
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
        prop_assert_eq!(add(a, b), add(b, a));
    }
    #[test]
    fn test_string_len(s in ".{0,100}") {
        prop_assert!(s.len() <= 100);
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.