Der Borrow Checker ist die Compiler-Phase, die jeden Borrow in deinem Code prüft und sicherstellt, dass die Aliasing-XOR-Mutability-Regel eingehalten wird. Was anfangs wie ein launischer Kritiker wirkt, ist eines der mächtigsten Werkzeuge der Sprache: er fängt komplette Klassen von Bugs (Data Races, Use-after-Free, iterator invalidation) zur Compile-Zeit ab. Dieser Artikel zeigt, wie der Borrow Checker mental funktioniert, was die Non-Lexical Lifetimes (NLL) seit Edition 2018 verändert haben, geht durch die fünf häufigsten Fehler-Codes mit ausführlichen Erklärungen und gibt konkrete Strategien zum Beheben jeder Konflikt-Klasse.

Was der Borrow Checker macht

Der Borrow Checker ist eine statische Analyse-Phase im Rust-Compiler. Er läuft nach Type-Check und vor Code-Generierung. Für jeden Wert in deinem Programm verfolgt er:

  • Wer besitzt ihn? (Ownership)
  • Welche aktiven Borrows existieren? (Shared vs. Mut)
  • Wann werden sie wieder freigegeben? (Lifetime)

Wenn irgendwo eine Operation die Aliasing-XOR-Mutability-Regel verletzen würde, wird sie als Compile-Fehler abgelehnt.

Was er nicht prüft

  • Logische Korrektheit — er garantiert nur Memory-Safety, nicht dass dein Algorithmus richtig ist.
  • Panic-Vermeidungunwrap() auf None, Index out of bounds. Das sind Laufzeit-Probleme.
  • Memory-Leaksstd::mem::forget, Rc-Zyklen. Safe Rust kann leaken, ohne Memory-Safety zu verletzen.
  • unsafe-Blöcke — innerhalb von unsafe { ... } darfst du Regeln verletzen, auf eigene Verantwortung.

Non-Lexical Lifetimes (NLL)

Vor Rust 1.31 (Edition 2018) hat der Borrow Checker lexikalisch gearbeitet: ein Borrow lebte bis zum Ende seines Scopes (}). Das führte zu vielen unnötigen Konflikten.

Mit NLL ist der Checker flow-sensitive: ein Borrow lebt nur bis zu seiner letzten Verwendung im Code-Pfad.

Rust Vor NLL: Konflikt — heute: ok
fn main() {
    let mut v = vec![1, 2, 3];
    let r = &v[0];
    println!("{r}");           // letzte Verwendung von r
    v.push(4);                  // ok — r ist „tot" ab hier
    println!("{:?}", v);
}

Lexikalisch wäre r noch bis zum Funktions-Ende „aktiv" — und damit v.push(4) ein Konflikt. NLL erkennt, dass r nach println! nicht mehr verwendet wird.

NLL macht idiomatischen Rust-Code drastisch einfacher und ist heute Default. Du musst dich nicht aktiv darum kümmern; gut zu wissen ist nur: der Compiler ist klüger, als die Regeln auf den ersten Blick suggerieren.

E0499 — cannot borrow as mutable more than once

Zwei &mut T auf denselben Wert gleichzeitig.

Rust E0499
fn main() {
    let mut s = String::from("Hi");
    let r1 = &mut s;
    let r2 = &mut s;        // E0499 — zweites &mut
    r1.push_str("!");
}

Diagnose:

Rust
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:4:18
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
5 |     r1.push_str("!");
  |     -- first borrow later used here

Behebung:

  • Den ersten Borrow erst zu Ende führen, dann den zweiten erstellen.
  • Wenn beide Refs disjunkte Teile betreffen (v[0] und v[1]): split_at_mut.
  • Bei verschachtelten Aufrufen: Zwischenwerte in lokale Bindungen extrahieren.

E0502 — cannot borrow as mutable because also borrowed as immutable

&T und &mut T gleichzeitig auf denselben Wert.

Rust E0502
fn main() {
    let mut v = vec![1, 2, 3];
    let r1 = &v[0];             // shared Borrow auf v
    v.push(4);                   // E0502 — &mut v braucht push
    println!("{r1}");
}

Diagnose:

Rust
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let r1 = &v[0];
  |              -- immutable borrow occurs here
4 |     v.push(4);
  |     ^^^^^^^^^ mutable borrow occurs here
5 |     println!("{r1}");
  |               ---- immutable borrow later used here

Behebung:

  • Die letzte Verwendung des shared Borrow vor den mutablen Aufruf ziehen (NLL macht das oft sichtbar).
  • Wert kopieren statt referenzieren: let r1 = v[0]; (bei Copy-Typen).
  • Falls beides parallel nötig: Cell/RefCell (Interior Mutability) — Smart-Pointer-Kapitel.

E0506 — cannot assign to borrowed value

Direkt-Mutation eines Werts, der gerade geborgt ist.

Rust E0506
fn main() {
    let mut x = 5;
    let r = &x;
    x = 10;             // E0506 — x ist shared geborgt durch r
    println!("{r}");
}

Diagnose:

Rust
error[E0506]: cannot assign to `x` because it is borrowed
 --> src/main.rs:4:5
  |
3 |     let r = &x;
  |             -- `x` is borrowed here
4 |     x = 10;
  |     ^^^^^^ `x` is assigned to here but it was already borrowed
5 |     println!("{r}");
  |               --- borrow later used here

Behebung:

  • Borrow vor der Zuweisung beenden.
  • Bei mehreren Borrows: explizit als Block gruppieren.

E0596 — cannot borrow as mutable

Versuch, eine &mut-Referenz von einer non-mut-Bindung zu nehmen.

Rust E0596
fn main() {
    let s = String::from("Hi");      // ohne mut
    let r = &mut s;                  // E0596
    r.push_str("!");
}

Diagnose:

Rust
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
3 |     let r = &mut s;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut s = String::from("Hi");
  |         +++

Behebung:

  • let mut s = ...; statt let s = ...;. Der Compiler zeigt die exakte Stelle.

E0716 — temporary value dropped while borrowed

Eine Referenz auf einen temporären Wert, der zu früh gedroppt wird.

Rust E0716
fn liefere_string() -> String { String::from("Hi") }

fn main() {
    let r = &liefere_string();        // E0716 — Temporary lebt nicht lang genug
    // Korrektur: erst in Bindung speichern, dann referenzieren
    // let s = liefere_string();
    // let r = &s;
    println!("{r}");
}

Tatsächlich verlängert Rust hier oft die Lebenszeit der Temporary („Temporary Lifetime Extension"), sodass dieser Code in einfachen Fällen funktioniert. Bei komplexeren Patterns greift E0716:

Rust Komplexer Fall
fn main() {
    let r;
    {
        let s = String::from("temp");
        r = &s;                       // s lebt nur im inneren Block
    }
    // println!("{r}");                // E0597 — s ist tot
}

Behebung:

  • Den temporären Wert in eine eigene let-Bindung mit ausreichendem Scope packen.
  • Falls die Funktion eine Referenz zurückgibt: prüfen, ob die Funktion einen owned Wert zurückgeben sollte.

Strategien zum Beheben von Borrow-Konflikten

Fünf Reflexe, mit denen sich die meisten Borrow-Fehler systematisch lösen:

1. Borrow-Lebenszeit verkürzen

Rust Block-Scope
fn main() {
    let mut v = vec![1, 2, 3];
    {
        let r = &v[0];           // r lebt nur in diesem Block
        println!("{r}");
    }
    v.push(4);                    // ok — r ist weg
}

Block-Klammern um den Borrow zwingen ihn zu früherem Drop.

2. Wert extrahieren statt referenzieren

Rust Wert kopieren
fn main() {
    let mut v = vec![1, 2, 3];
    let erstes = v[0];           // Copy, kein Borrow
    v.push(4);                    // ok
    println!("{erstes}");
}

Bei Copy-Typen reicht eine Direkt-Kopie statt einer Referenz.

3. Splitting mit split_at_mut oder Iterator-Indices

Rust Disjunkte Borrows
fn tausch_erste_und_letzte(v: &mut [i32]) {
    if v.len() < 2 { return; }
    let len = v.len();
    let (links, rechts) = v.split_at_mut(len - 1);
    std::mem::swap(&mut links[0], &mut rechts[0]);
}

split_at_mut teilt einen Slice in zwei garantiert disjunkte mutable Slices.

4. Interior Mutability mit Cell/RefCell

Wenn die Borrow-Regeln zu Compile-Zeit nicht ausreichen, gibt es Container, die die Prüfung zur Laufzeit verschieben:

Rust RefCell
use std::cell::RefCell;

fn main() {
    let zaehler = RefCell::new(0);
    // shared Ref, aber Mutation möglich:
    *zaehler.borrow_mut() += 1;
    *zaehler.borrow_mut() += 1;
    println!("{}", zaehler.borrow());     // 2
}

Achtung: RefCell panickt zur Laufzeit, wenn doppelt mutable geborgt wird. Es verschiebt die Borrow-Prüfung nur zeitlich. Mehr im Smart-Pointer-Kapitel.

5. Funktion umstrukturieren

Manchmal ist der Konflikt ein Code-Smell — die Funktion macht zu viel. Aufspalten in zwei Funktionen, eine liest, die andere schreibt, löst den Knoten.

Praxis: Borrow Checker im echten Code

Vec mutieren während Lesen

Rust Index-Sammlung
fn entferne_duplikate(v: &mut Vec<i32>) {
    // Erst Indices der Duplikate sammeln, dann entfernen.
    let mut zu_entfernen = Vec::new();
    let mut gesehen = std::collections::HashSet::new();

    for (i, &val) in v.iter().enumerate() {
        if !gesehen.insert(val) {
            zu_entfernen.push(i);
        }
    }

    // Von hinten nach vorne entfernen (Indices bleiben gültig)
    for &i in zu_entfernen.iter().rev() {
        v.remove(i);
    }
}

Klassisches Pattern: &v für Lesen, danach &mut v für Modifikation. Die zwei Schritte sind durch separate Loops getrennt — kein Borrow-Konflikt.

Cache-Update mit explizitem Scope

Rust Cache-Pattern
use std::collections::HashMap;

fn lookup_oder_lade(cache: &mut HashMap<String, Vec<u8>>, key: &str) -> Vec<u8> {
    // Erst Lookup (shared borrow), dann ggf. insert (mut borrow) —
    // beide Borrows sind disjunkt im Zeitablauf.
    if let Some(daten) = cache.get(key) {
        return daten.clone();
    }
    // Hier ist der get-Borrow tot — neue Daten einfügen
    let daten = vec![0u8; 1024];        // teure Berechnung
    cache.insert(key.to_string(), daten.clone());
    daten
}

NLL erkennt, dass der get-Borrow nach dem return endet — danach ist insert mit &mut cache ok. Ohne NLL wäre das ein Konflikt.

Iter-Sum mit nachträglicher Mutation

Rust Read-then-Write
fn normalisieren(v: &mut Vec<f64>) {
    let summe: f64 = v.iter().sum();        // shared borrow für iter()
    if summe == 0.0 { return; }
    for x in v.iter_mut() {                 // jetzt mut borrow
        *x /= summe;
    }
}

Die iter()-Borrow endet mit der Auswertung von summe. Danach kann iter_mut() frei greifen. Klassisches Read-then-Write-Pattern.

Verschachtelte Datenstruktur mit split_at_mut

Rust Matrix-Operation
fn tausche_zeilen(matrix: &mut [Vec<i32>], i: usize, j: usize) {
    if i == j { return; }
    let (lo, hi) = if i < j { (i, j) } else { (j, i) };
    let (links, rechts) = matrix.split_at_mut(hi);
    std::mem::swap(&mut links[lo], &mut rechts[0]);
}

Zwei mutable Borrows auf verschiedene Zeilen einer Matrix — möglich durch split_at_mut.

Borrow-Konflikt durch Restrukturierung lösen

Rust Methoden-Aufteilung
struct Stats {
    werte: Vec<f64>,
    mittel: f64,
}

impl Stats {
    // Schlecht: &mut self UND iter() braucht &self gleichzeitig
    // pub fn aktualisiere(&mut self) {
    //     self.mittel = self.werte.iter().sum::<f64>() / self.werte.len() as f64;
    // }
    // Funktioniert tatsächlich — der Borrow Checker erkennt disjunkte Felder.

    // Noch klarer per Hilfsfunktion:
    pub fn aktualisiere(&mut self) {
        self.mittel = Self::berechne_mittel(&self.werte);
    }
    fn berechne_mittel(werte: &[f64]) -> f64 {
        werte.iter().sum::<f64>() / werte.len() as f64
    }
}

Wenn der Borrow Checker zu komplex wird: Berechnung in eine static Hilfsfunktion auslagern, die nur Slice-Parameter nimmt. Macht den Code auch testbarer.

Map-Update mit Entry-API

Rust Entry-Pattern
use std::collections::HashMap;

fn inkrementieren(zaehler: &mut HashMap<String, u32>, key: &str) {
    // Idiomatisch mit Entry-API — ein Borrow für Lookup + ggf. Insert
    *zaehler.entry(key.to_string()).or_insert(0) += 1;
}

Die entry-API gibt einen mutable Borrow auf den Entry-Wert zurück und löst gleichzeitig „insert falls nicht da". Borrow-konfliktfrei.

Zwei Werte aus einem Vec gleichzeitig

Rust Doppel-Index mit Indirektion
fn paar_verarbeiten(v: &mut Vec<i32>, i: usize, j: usize) {
    // v[i] und v[j] gleichzeitig mutable — direkt geht NICHT.
    // Lösung: split_at_mut, oder einzeln per swap-Trick:
    if i < j {
        let (links, rechts) = v.split_at_mut(j);
        links[i] += rechts[0];
    }
}

Direkter v[i] + v[j] mit beidem mutable scheitert am Borrow Checker. split_at_mut ist die idiomatische Lösung.

State-Machine mit lokalen Bindings

Rust State-Trans
#[derive(Debug)]
enum State { Init, Aktiv(String), Beendet }

fn weiter(state: &mut State) {
    // Wert herausnehmen, neuen einsetzen — mem::replace
    let alt = std::mem::replace(state, State::Beendet);
    *state = match alt {
        State::Init => State::Aktiv(String::from("start")),
        State::Aktiv(s) => {
            println!("verlasse Aktiv mit: {s}");
            State::Beendet
        }
        State::Beendet => State::Beendet,
    };
}

mem::replace nimmt den alten Wert heraus (gibt ihn als owned zurück) und ersetzt mit einem temporären — danach kann frei mit dem alten Wert gematcht werden. Klassisches State-Machine-Idiom.

Interessantes

NLL macht idiomatischen Code möglich.

Vor NLL waren viele natürlich wirkende Patterns Borrow-Konflikte — etwa „lies, dann schreib" in einer Zeile. Heute erkennt der Checker die letzte Verwendung eines Borrows und beendet ihn dort. Bei alten Tutorials, die Borrow-Konflikte zeigen, immer prüfen: gilt das heute noch?

Fehler-Codes lassen sich mit rustc --explain nachschlagen.

rustc --explain E0502 zeigt eine ausführliche Erklärung mit Beispiel-Code und Lösung. Sehr lehrreich beim Lernen — und auch später, wenn man ein selten gesehenes Fehler-Code-Format trifft.

Disjunkte Struct-Felder darfst du parallel mutable borgen.

let rx = &mut p.x; let ry = &mut p.y; ist erlaubt. Der Borrow Checker analysiert pro Feld. Bei Vec-Indices klappt das nicht — der Checker kann v[0] und v[1] nicht als disjunkt erkennen.

iter hält den Container shared geborgt.

Während eines for x in &v { ... }-Loops ist v als shared geborgt. Im Loop-Body kann v nicht mutable verwendet werden. Für In-Place-Mutation: iter_mut() benutzen.

RefCell ist die Notbremse, nicht der Standard.

Wenn der Compile-Time-Borrow-Check zu strikt für eine spezifische Situation ist, gibt es RefCell (Single-Threaded) oder Mutex (Multi-Threaded). Sie verschieben die Prüfung zur Laufzeit. Nutze sie gezielt — nicht als „macht den Compiler still"-Workaround.

Borrow-Konflikte sind oft Symptome von schlechtem Design.

Wenn der Borrow Checker hartnäckig klagt, lohnt ein Blick aufs Design. Hat eine Funktion zu viel Verantwortung? Sollte ein Wert in zwei separate Felder aufgeteilt werden? Oft löst Refactoring den Konflikt eleganter als technische Tricks.

Reborrowing wird oft implizit gemacht.

Wenn du eine &mut-Referenz an eine Funktion gibst, fügt der Compiler oft ein Reborrow ein. Damit ist die Original-Referenz nach dem Call wieder nutzbar. Mehr im Reborrowing-Artikel.

Der Borrow Checker macht keinen Lauf-Zeit-Overhead.

Die ganze Analyse ist Compile-Zeit. Das fertige Binary hat keinen einzigen Check eingebaut, der die Aliasing-Regeln zur Laufzeit prüft (außer du nutzt RefCell/Mutex explizit). Die Garantien sind kostenlos.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu References & Borrowing

Zur Übersicht