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

Der Borrow Checker ist mächtig, aber nicht allmächtig. Es lohnt sich zu wissen, welche Bug-Klassen er nicht abfängt, damit du keine falschen Erwartungen entwickelst:

Logische Korrektheit — er garantiert nur Memory-Safety, nicht dass dein Algorithmus richtig ist. Wenn deine Funktion das Falsche berechnet oder den falschen Wert zurückgibt, wird der Compiler das nicht erkennen. Tests bleiben unverzichtbar.

Panic-Vermeidungunwrap() auf einem None, ein Out-of-Bounds-Index, eine Division durch Null. Solche Bugs sind Laufzeit-Probleme; der Borrow Checker prüft nur Memory-Aliasing, nicht Wert-Domänen.

Memory-Leaksstd::mem::forget, Rc-Zyklen, vergessene Drop-Implementierungen. Safe Rust kann Memory leaken, ohne dass eine der Ownership-Regeln verletzt wäre. Memory-Leaks sind eine ärgerliche Form von Bug, aber sie verletzen keine Memory-Safety-Garantien.

unsafe-Blöcke — innerhalb von unsafe { ... } darfst du Dinge tun, die der Borrow Checker normalerweise verbieten würde: rohe Pointer dereferenzieren, mutable Statics modifizieren, FFI-Aufrufe machen. Die Verantwortung für Memory-Safety liegt dann beim Programmierer.

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. Drei Lösungswege, je nach Situation:

Wenn die beiden Borrows zeitlich entkoppelt werden können, hilft die einfachste Lösung: den ersten Borrow erst zu Ende führen, dann den zweiten erstellen. Bei NLL endet ein Borrow an seiner letzten Verwendung, also reicht oft, die Reihenfolge der Operationen zu ändern.

Wenn beide Borrows disjunkte Teile derselben Sammlung betreffen — etwa v[0] und v[1] —, ist split_at_mut die idiomatische Antwort. Diese Methode teilt einen Slice in zwei garantiert disjunkte mutable Sub-Slices, sodass der Compiler die Aliasing-Garantie selbst behält.

Wenn der Konflikt aus verschachtelten Method-Calls entsteht (etwa v.foo(v.bar()) mit bar und foo beide auf v), extrahiere Zwischenwerte in lokale Bindungen. Statt v.push(v.len()) schreibe let len = v.len(); v.push(len);. Die explizite Zwischenvariable löst den Borrow auf.

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 Lösung hängt davon ab, ob der shared Borrow wirklich parallel zum mut-Borrow leben muss:

In den meisten Fällen ist die Antwort nein — du musst nur die letzte Verwendung des shared Borrow vor den mutablen Aufruf ziehen. Dank NLL endet der Borrow dann an dieser letzten Verwendung, und die folgende Mutation ist legal. Oft reicht es, die Reihenfolge von println!-Aufrufen umzustellen.

Bei Copy-Typen kannst du den Wert kopieren statt referenzieren: let r1 = v[0]; (mit Copy-Typ-Element) gibt dir keinen Borrow, sondern eine Kopie. Danach kannst du frei auf v mutieren. Bei größeren Typen funktioniert das nicht direkt, aber .clone() ist der explizite Weg.

Falls beides wirklich parallel nötig ist — etwa weil zwei Code-Pfade gleichzeitig Lesen und Schreiben müssen —, sind Cell oder RefCell die Notbremse. Sie verschieben die Borrow-Prüfung von Compile-Zeit zu Laufzeit. Das ist eine reife Lösung für spezielle Fälle, aber kein Default — wer RefCell überall einsetzt, hat das Borrow-Modell nicht wirklich verstanden.

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. E0506 ist meist eine Variante von E0502, nur ausgedrückt durch direktes Schreiben auf den Original-Wert statt durch Method-Call. Die Lösungen sind dieselben: den Borrow vor der Zuweisung beenden (durch Umstellen der Reihenfolge, sodass die letzte Verwendung vor der Mutation liegt), oder die Borrows in einen expliziten Block gruppieren, sodass sie am Block-Ende sicher gedroppt sind.

Bei sehr alten Codebases (vor NLL) findet man oft Block-Klammern, die genau diese Lebenszeit-Begrenzung erzwingen sollten. Heute braucht man das selten, aber als bewusste Strukturierungs-Maßnahme ist es immer noch valide.

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. Die Lösung ist unkompliziert: ergänze das mut-Schlüsselwort an der let-Stelle, also let mut s = ...; statt let s = ...;. Der Compiler zeigt dir mit der consider changing this to be mutable-Hint genau die Stelle, an der die Änderung erfolgen muss.

Wenn du den Fehler regelmäßig siehst, lohnt ein Blick auf die API: warum erwartet die Methode eine mutable Referenz, obwohl du den Wert nur lesen willst? Manchmal gibt es eine &self-Alternative, die du eigentlich verwenden wolltest. Die consider-Hint ist hilfreich, aber nicht immer die semantisch beste Lösung.

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. Wenn das Problem temporäre Werte sind, die zu früh gedroppt werden, gibt es zwei Strategien:

Der direkte Weg: den temporären Wert in eine eigene let-Bindung mit ausreichendem Scope packen. Statt let r = &funktion(); schreibst du let s = funktion(); let r = &s; — die Bindung s lebt jetzt für den ganzen umgebenden Scope, und r referenziert sie sicher.

Der semantische Weg: prüfen, ob die Funktion einen owned Wert zurückgeben sollte. Wenn der Aufrufer einen langlebigen Wert braucht, ist eine Referenz auf eine kurzlebige Temporary die falsche API. Eine Funktion, die String zurückgibt, ist meist klarer als eine, die &str mit einem Lifetime-Parameter zurückgibt — auch wenn die Variante mit Borrow eventuell schneller wäre.

Strategien zum Beheben von Borrow-Konflikten

Über die fünf häufigsten Fehler-Codes hinaus lohnt es sich, ein paar generelle Reflexe zu entwickeln. Wer einen Borrow-Konflikt sieht, sollte diese fünf Strategien in der Reihenfolge durchgehen — meistens passt eine davon.

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
}

Vor NLL war das die Standard-Technik: explizite Block-Klammern um den Borrow zwingen ihn zu früherem Drop. Heute, mit NLL, wird das selten gebraucht — der Compiler erkennt die letzte Verwendung automatisch. Aber als bewusste Strukturierungs-Maßnahme ist es immer noch sinnvoll, etwa wenn ein Reviewer auf einen Blick sehen soll, wo der Borrow endet. Bei besonders komplexen Funktionen mit vielen Borrows kann ein expliziter Block die Lebenszeit-Logik dokumentieren.

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 wie i32, bool, char reicht eine Direkt-Kopie statt einer Referenz. let erstes = v[0] (statt &v[0]) liefert einen unabhängigen Wert, der keinen Borrow auf v hält — danach kann v frei mutiert werden. Bei größeren Typen oder Heap-Typen funktioniert das nicht ohne clone, aber für die häufigen kleinen Primitive ist es die eleganteste Lösung.

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. Im Beispiel oben werden das erste und das letzte Element einer Liste vertauscht — etwas, das ohne split_at_mut mit zwei separaten &mut-Borrows nicht ginge. Die Methode ist die Stdlib-Antwort auf das häufigste „ich brauche zwei mutable Borrows in dieselbe Sammlung"-Problem. Analog gibt es bei Vec und HashMap weitere spezialisierte Methoden wie iter_mut().enumerate() oder get_disjoint_mut.

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
}

RefCell<T> ist die Compile-Zeit-Borrow-Prüfung als Laufzeit-Check. Du kannst auf einem RefCell durch eine &self-Referenz borrow_mut() aufrufen und damit den Inhalt verändern — was den Borrow Checker auf Typ-Ebene zufriedenstellt. Zur Laufzeit zählt der RefCell die aktiven Borrows mit; bei einem Doppel-Mutable-Borrow panickt er.

Achtung: Das ist eine sinnvolle Lösung für spezifische Fälle, aber kein generelles „macht den Compiler still"-Tool. Wer überall RefCell einsetzt, verliert die Compile-Zeit-Garantien und tauscht sie gegen Laufzeit-Panics ein. Faustregel: erst alle anderen Strategien probieren; RefCell nur, wenn es semantisch wirklich gerechtfertigt ist (etwa für Observer-Pattern, Bidirectional References, oder GUI-Code mit zirkulärer State-Logik).

5. Funktion umstrukturieren

Manchmal ist der Borrow-Konflikt ein Symptom für ein Design-Problem, nicht für eine zu strenge Regel. Wenn eine Funktion gleichzeitig Lesen und Schreiben auf demselben Wert braucht, macht sie vielleicht zu viel auf einmal. Die Lösung ist Refactoring: zwei separate Funktionen, eine liest und extrahiert die nötigen Informationen, die andere schreibt mit den extrahierten Werten.

Diese Methode ist oft die beste, auch wenn sie technisch nicht zwingend ist. Sie führt zu klarerer Code-Struktur, besserer Testbarkeit (jede Funktion hat eine kleinere Verantwortung) und nebenbei zu Borrow-Konflikt-Freiheit. Wer den Borrow Checker als Design-Feedback ernst nimmt, schreibt fast immer besseren Code.

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

Das ist das klassische Read-then-Write-Pattern, das viele Borrow-Konflikte auflöst: erst alle nötigen Informationen lesend sammeln (in der ersten Schleife mit &v), danach die Modifikation in einem zweiten Durchgang (mit &mut v). Die zwei Phasen sind im Code klar getrennt, sodass kein Borrow gleichzeitig auf der lesenden und der schreibenden Seite gehalten wird.

Eine subtile Detail: das Entfernen läuft von hinten nach vorne (zu_entfernen.iter().rev()). Das ist wichtig, weil Vec::remove(i) alle Elemente nach Position i nach vorne verschiebt — wenn wir von vorne entfernen würden, würden die noch zu entfernenden Indices ungültig werden. Von hinten zu arbeiten ist das idiomatische Idiom für „Indices in einer mutierten Sammlung beibehalten".

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
}

Diese Funktion zeigt, wie NLL Code natürlich lesbar macht. Der cache.get(key)-Aufruf erzeugt zunächst einen shared Borrow auf die Map. Wenn der if let Some(daten) = ...-Zweig greift, wird daten.clone() zurückgegeben und die Funktion endet — der shared Borrow wird mit dem Return aufgelöst.

Wenn der Zweig nicht greift (also None), endet der shared Borrow nach dem if let — die Map ist wieder frei. Danach folgt cache.insert(...) mit mutable Borrow. Vor NLL wäre das ein Konflikt gewesen, weil der get-Borrow lexikalisch bis zum Funktions-Ende gelten würde. Mit NLL erkennt der Compiler die genaue Lebenszeit und lässt den Code zu.

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 zwei Schleifen — eine lesend (iter().sum()), eine schreibend (iter_mut()) — sind durch eine einfache lokale Variable getrennt. Sobald die Summe in summe ist, kann der Iterator gedropt werden, und der mutable Iterator-Loop läuft konfliktfrei. Das ist eine Variante des Read-then-Write-Patterns von oben, hier nicht über Indizes, sondern über direkte Iteration.

Wer Performance kritisch braucht, kann diese zwei Loops nicht zu einem fusionieren — der Compiler erlaubt es nicht, und es wäre semantisch auch falsch, weil die Summe vor der Normalisierung berechnet sein muss. Manche Operationen brauchen einfach zwei Durchgänge.

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]);
}

Eine typische Matrix-Operation: zwei Zeilen vertauschen. Ohne split_at_mut würde Rust dir nicht erlauben, gleichzeitig &mut matrix[i] und &mut matrix[j] zu halten — beide sind formal mutable Borrows auf dieselbe Outer-Sammlung. Mit split_at_mut(hi) zerlegst du die Matrix in zwei disjunkte Teile (links und rechts), und der Compiler weiß, dass deren mutable Borrows nicht aliasen können.

Das if i == j { return; } am Anfang ist eine Safety-Net: bei identischen Indices wäre der Split bei hi == lo, und die folgende Logik würde zwei mutable Borrows auf dasselbe Element verlangen — was selbst split_at_mut nicht ermöglicht. Das frühe Return ist defensiv und idiomatisch.

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 komplexen Konflikten führt, lohnt sich die Strategie, die Berechnung in eine statische Hilfsfunktion auszulagern. berechne_mittel(werte: &[f64]) nimmt nur einen Slice-Parameter — keine Methode, kein &self. Damit ist klar: diese Funktion fasst nur den Slice an, nicht den umgebenden Struct.

Die ursprüngliche Variante mit self.mittel = self.werte.iter().sum::<f64>() / ... würde tatsächlich kompilieren, weil der Compiler disjunkte Felder erkennt. Aber die Hilfsfunktions-Variante ist trotzdem besser: sie macht den Datenfluss explizit, ist isoliert testbar, und schützt vor zukünftigen Borrow-Konflikten, falls die Berechnung später wächst.

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 ist ein Paradebeispiel dafür, wie eine durchdachte Library-API Borrow-Konflikte elegant löst. Statt zwei separate Operationen — erst contains_key, dann insert oder get_mut —, die jeweils einen Borrow auf die Map brauchen, kombiniert entry beide in einem Borrow. Das or_insert(0) liefert einen &mut u32 auf den Wert (entweder den existierenden oder den neu eingefügten), und *= ... += 1 modifiziert ihn direkt.

Diese Art von kombinierten APIs gibt es überall in der Stdlib, wenn der Designer wusste, dass die zwei Schritte „check + modify" der häufige Use-Case sind. Vec::resize, HashMap::entry, BTreeMap::entry — alle umgehen Borrow-Konflikte durch atomares Verhalten.

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];
    }
}

Eine häufige Aufgabe in numerischem Code: zwei Elemente eines Vec verarbeiten, eines davon modifizieren. Der direkte Weg v[i] += v[j] wäre Borrow-Konflikt, weil sowohl &mut v[i] als auch &v[j] einen Borrow auf den ganzen Vec haben (der Borrow Checker prüft nicht auf Index-Ebene).

Die split_at_mut(j)-Lösung teilt den Vec an Position j: alles vor j liegt in links, alles ab j in rechts. Damit sind links[i] und rechts[0] (was v[j] entspricht) zwei garantiert disjunkte Borrows. Die Voraussetzung ist i < j — sonst wäre die Split-Logik anders. Bei i > j müsste die Reihenfolge der Split-Argumente umgedreht werden.

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,
    };
}

State-Machine-Übergänge mit Enums sind eine der häufigsten Stellen, an denen mem::replace brilliert. Das Problem: wir haben nur &mut state, aber wir wollen den alten Wert in einem match destrukturieren — das geht nicht direkt, weil match state { ... } einen Move des Werts erwarten würde, was bei &mut nicht erlaubt ist.

mem::replace(state, State::Beendet) löst es atomar: der State wird mit State::Beendet ersetzt, und der alte Wert wird als owned zurückgegeben. Jetzt können wir mit alt frei matchen und destrukturieren — insbesondere den String aus State::Aktiv herausnehmen, der bei direktem Match-Versuch nicht hätte gemoved werden können. Am Ende des Match wird der eigentliche neue State gesetzt.

Das Pattern ist sehr typisch und elegant, wenn State-Übergänge inhalt-haltige Enum-Varianten beinhalten. Ohne mem::replace wäre die Alternative entweder clone() (verschwenderisch bei großen Werten) oder eine komplette Restrukturierung des States.

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