Reborrowing ist eines der subtilsten Konzepte in Rusts Reference-System. Es löst ein scheinbares Paradox: &mut T ist nicht Copy, aber trotzdem kannst du eine &mut-Referenz an mehrere Funktionen nacheinander übergeben, ohne sie zu „verbrauchen". Wie? Der Compiler fügt automatisch einen Reborrow ein — eine neue, kurzlebigere &mut-Referenz, die nur für die Dauer des Funktionsaufrufs existiert. Die ursprüngliche Referenz wird danach wieder nutzbar. Dieser Artikel zerlegt den Mechanismus, zeigt, wann er greift und wann nicht, und in welchen Situationen du Reborrowing explizit anwenden musst.
Das Paradox: &mut ist nicht Copy
Du erinnerst dich aus dem Shared-vs-Mut-Artikel: &T ist Copy, &mut T nicht. Trotzdem funktioniert das:
fn drucken(r: &mut String) {
println!("{r}");
}
fn modifizieren(r: &mut String) {
r.push_str("!");
}
fn main() {
let mut s = String::from("Hi");
let r = &mut s;
drucken(r);
modifizieren(r); // r noch nutzbar — wie das?
drucken(r); // und nochmal
println!("{r}");
}Wenn &mut wirklich nicht Copy wäre und beim ersten drucken(r) gemoved würde, dürfte modifizieren(r) nicht mehr funktionieren. Aber es funktioniert. Warum?
Antwort: Reborrowing. Der Compiler übersetzt drucken(r) intern zu drucken(&mut *r). Das &mut *r ist ein neuer, kurzlebiger Borrow auf den gleichen Wert wie r — er lebt nur für die Dauer des Funktionsaufrufs. Nach dem Return ist die neue Referenz weg, r wird wieder „aktiviert".
Was Reborrowing genau ist
Ein Reborrow ist eine neue Referenz, die aus einer existierenden Referenz abgeleitet wird — keine Kopie, sondern ein neuer Borrow mit eigener, kürzerer Lebenszeit. Konzeptuell läuft folgender Ablauf: die Original-Referenz wird kurz „beiseitegelegt", eine neue Referenz auf denselben Wert mit einer engeren Lifetime wird erstellt, sie wird genutzt, und danach wird die Original-Referenz wieder freigegeben. Während die neue Referenz lebt, ist die Original-Referenz nicht direkt verwendbar — der Borrow Checker sorgt dafür, dass die Aliasing-XOR-Mutability-Regel auch hier eingehalten wird.
fn main() {
let mut s = String::from("Hi");
let r1: &mut String = &mut s;
let r2: &mut String = &mut *r1; // explizites Reborrow
r2.push_str("!");
// r1 noch nutzbar, sobald r2 „tot" ist
r1.push_str("?");
}Was hier passiert:
r1ist eine mutable Referenz aufs. Solanger1aktiv ist, istsexklusiv geborgt.&mut *r1erstellt eine neue mutable Referenz, die sich an die kurze Lebenszeit vonr2koppelt.- Während
r2lebt, istr1„eingefroren" — der Borrow Checker erlaubt keinen Zugriff überr1. - Wenn
r2nicht mehr verwendet wird (NLL), istr1wieder aktiv.
Das Schöne an Rusts Design: der Compiler fügt diese Reborrow-Operation automatisch ein, wenn du eine &mut-Referenz an eine Funktion übergibst. Du schreibst funktion(r), intern wird daraus funktion(&mut *r). Damit funktioniert die natürliche Schreibweise, ohne dass du den Mechanismus selbst kennen musst — du merkst nur, dass deine &mut-Referenz nach Funktions-Calls weiter benutzbar bleibt, obwohl &mut formal nicht Copy ist.
In sehr seltenen Fällen — etwa bei verschachtelten Closures, manchen Generic-Kontexten oder wenn die implizite Konvertierung nicht greift — musst du den Reborrow selbst schreiben. Dann hilft die Syntax &mut *r.
Implizite Reborrows bei Funktions-Calls
fn doppeln(r: &mut Vec<i32>) {
for x in r.iter_mut() { *x *= 2; }
}
fn main() {
let mut v = vec![1, 2, 3];
let r = &mut v;
doppeln(r); // Reborrow → r überlebt den Call
doppeln(r); // funktioniert nochmal
println!("{:?}", r);
}Was der Compiler hieraus macht:
fn main() {
let mut v = vec![1, 2, 3];
let r = &mut v;
doppeln(&mut *r); // implizites Reborrow eingefügt
doppeln(&mut *r);
}Das &mut *r ist ein „Re-Borrow durch r" — neue Referenz mit eigener (kürzerer) Lifetime. Während sie aktiv ist (also für den Call), kann r nicht direkt benutzt werden. Sobald der Call endet, ist r wieder frei.
Reborrow für &T — kein Problem
Bei &T ist Reborrowing weniger sichtbar, weil &T Copy ist:
fn drucke(r: &String) {
println!("{r}");
}
fn main() {
let s = String::from("Hi");
let r = &s;
drucke(r); // copy (oder reborrow — ergibt dasselbe)
drucke(r); // funktioniert problemlos
}Bei &T denkst du an „Kopieren der Referenz". Bei &mut T ist es technisch ein Reborrow — aber das Resultat ist ähnlich: die Original-Referenz bleibt nutzbar.
Wann Reborrow NICHT funktioniert
Reborrowing klappt nicht immer. Klassischer Fall: wenn die Funktion die Referenz festhalten will (z. B. als Rückgabe-Lifetime an den Aufrufer):
fn behalte(r: &mut String) -> &mut String {
r // Funktion gibt Referenz mit gleicher Lifetime zurück
}
fn main() {
let mut s = String::from("Hi");
let r1 = &mut s;
let r2 = behalte(r1); // r2 hat NICHT die Lifetime eines Reborrows
// r1 ist hier ein „verschluckt" — Verwendung wäre Konflikt mit r2
r2.push_str("!");
// println!("{r1}"); // Fehler
}Hier passiert kein Reborrow, sondern eine Art „Move" der &mut-Referenz. r1 ist nach dem Call praktisch nicht mehr nutzbar, weil r2 „seine Stelle eingenommen" hat.
Wann der Compiler reborrowt
Faustregel: Reborrowing passiert, wenn die Funktion keine Referenz mit der gleichen Lifetime wie der Parameter zurückgibt. Wenn die Signatur eine Rückgabe-Referenz mit der Parameter-Lifetime hat, fällt der Compiler auf die strikteren Regeln zurück.
// Reborrow funktioniert — Funktion gibt nichts mit Lifetime des Parameters zurück:
fn fall_a(_r: &mut String) {}
// Reborrow funktioniert nicht — Funktion „nimmt" die Lifetime mit:
fn fall_b<'a>(r: &'a mut String) -> &'a mut String { r }In Fall A wird _r nach dem Call sofort gedroppt — kein Konflikt mit dem Aufrufer-r. In Fall B trägt das Ergebnis die Lifetime des Parameters mit — der Aufrufer-r ist solange „blockiert", wie das Ergebnis lebt.
Manuelles Reborrow
Manchmal musst du Reborrow explizit machen — z. B. wenn du eine &mut-Referenz in einer Schleife oder mehrfach in einem Block weiterreichen willst:
fn arbeite(r: &mut Vec<i32>) {
r.push(42);
}
fn main() {
let mut v = vec![1, 2, 3];
let r = &mut v;
for _ in 0..3 {
arbeite(&mut *r); // explizites Reborrow in der Schleife
}
println!("{:?}", r);
}In manchen Fällen reicht hier auch arbeite(r) — der Compiler fügt das &mut * automatisch ein. Wenn nicht: explizit machen.
Reborrow eines Slice
fn arbeite(s: &mut [i32]) {
for x in s.iter_mut() { *x += 1; }
}
fn main() {
let mut v = vec![1, 2, 3, 4];
let s: &mut [i32] = &mut v[..];
arbeite(&mut *s); // explizites Reborrow
arbeite(s); // auch ok — implizites Reborrow
println!("{:?}", s);
}Reborrow vs. Move bei Variablen-Zuweisung
Wenn du eine &mut-Referenz einer neuen Bindung zuweist, passiert typischerweise ein Move, kein Reborrow:
fn main() {
let mut s = String::from("Hi");
let r1 = &mut s;
let r2 = r1; // MOVE — r1 ist „verbraucht"
// r1.push_str("!"); // Fehler — gemoved
r2.push_str("!");
}Bei let r2 = r1 ist die Default-Annahme: Move. r1 ist danach weg. Wenn du eine zweite Referenz brauchst, die r1 „beibehält": explizites Reborrow.
fn main() {
let mut s = String::from("Hi");
let r1 = &mut s;
{
let r2 = &mut *r1; // Reborrow
r2.push_str("!");
}
// r2 ist hier weg, r1 wieder aktiv
r1.push_str("?");
println!("{r1}");
}Funktions-Calls reborrowen automatisch, Variablen-Zuweisungen moven
Das ist die wichtigste Faustregel:
funktion(r)mitr: &mut T→ impliziter Reborrow,rbleibt nutzbar.let r2 = rmitr: &mut T→ Move,rist weg.
Praxis: Reborrowing im echten Code
Mehrere Methoden-Calls auf demselben Empfänger
fn main() {
let mut s = String::from("Hi");
let r = &mut s;
r.push_str(" "); // impliziter Reborrow für jeden Call
r.push_str("Welt");
r.push_str("!");
println!("{r}");
}Hier siehst du Reborrowing in seiner unauffälligsten Form. Jeder r.push_str(...)-Aufruf läuft intern über die Methode String::push_str(&mut self, ...) — also nimmt sich &mut r. Ohne Reborrow wäre r nach dem ersten Call gemoved und nicht mehr verfügbar. Mit dem impliziten Reborrow ist r nach jedem Call sofort wieder frei.
Dieses Pattern ist so allgegenwärtig in Rust-Code, dass die meisten Programmierer es nie als „Reborrow" bezeichnen — es passiert einfach. Erst wenn man bewusst mit der Mechanik der Sprache arbeitet, sieht man, wie viele Sprachfeatures auf diesem unsichtbaren Mechanismus aufbauen.
Helper-Funktion im Loop
fn verarbeite_einen(buffer: &mut Vec<u8>, byte: u8) {
buffer.push(byte);
}
fn verarbeite_alle(buffer: &mut Vec<u8>, input: &[u8]) {
for &b in input {
verarbeite_einen(buffer, b); // impliziter Reborrow
}
}
fn main() {
let mut buf = Vec::new();
verarbeite_alle(&mut buf, &[1, 2, 3, 4]);
println!("{buf:?}");
}Eine klassische Schleifen-Anwendung: ein mutable Buffer wird wiederholt an eine Helper-Funktion übergeben. Ohne Reborrowing wäre der erste verarbeite_einen(buffer, b)-Call ein Move — die zweite Iteration könnte den Buffer nicht mehr verwenden. Mit dem impliziten Reborrow ist der Buffer nach jedem Aufruf wieder unangetastet verfügbar.
Wenn du jemals einen Loop schreibst, in dem du dieselbe &mut-Referenz mehrfach an eine Helper-Funktion gibst, ist Reborrowing der Grund, warum das überhaupt funktioniert. Ohne diesen Mechanismus müsste die Helper-Funktion die Referenz immer zurückgeben — was zu sehr umständlichen Signaturen führen würde.
Stream-Reader mit wiederholtem mut-Zugriff
use std::io::{self, Read};
fn lese_chunks(reader: &mut impl Read, buffer: &mut [u8]) -> io::Result<usize> {
let mut gesamt = 0;
loop {
let n = reader.read(buffer)?; // Reborrow von buffer
if n == 0 { break; }
gesamt += n;
}
Ok(gesamt)
}Innerhalb des Loops wird buffer per Reborrow an read weitergegeben. Nach dem Call ist buffer weiter benutzbar.
Database-Statement mit Connection-Reborrow
struct Connection;
impl Connection {
fn execute(&mut self, query: &str) -> usize {
println!("EXEC: {query}");
0
}
}
fn migrate(conn: &mut Connection) {
conn.execute("CREATE TABLE users (id INT)"); // Reborrow
conn.execute("CREATE INDEX idx_users ON users(id)"); // Reborrow
conn.execute("INSERT INTO users VALUES (1)"); // Reborrow
}In Datenbank-APIs ist das ein typisches Pattern: eine Verbindung wird mehrfach hintereinander für verschiedene Statements verwendet. Wäre die &mut Connection ohne Reborrowing, müsste jede einzelne execute-Methode die Connection als Move nehmen und zurückgeben — die API würde so aussehen: conn = conn.execute("..."). Das ist extrem umständlich und in real-world Code unbenutzbar.
Reborrowing erlaubt die natürliche Sequenz von Statements, die jeder, der schon mal mit SQL gearbeitet hat, intuitiv schreiben würde. Bibliotheken wie sqlx, diesel und rusqlite verlassen sich alle auf diesen Mechanismus.
Mutex-Guard mit Mehrfach-Mutation
use std::sync::Mutex;
fn increment_alle(items: &Mutex<Vec<i32>>) {
let mut guard = items.lock().unwrap();
// guard ist eine Art &mut Vec<i32>
guard.push(1);
guard.push(2);
guard.push(3);
// Jeder Method-Call reborrowt
}MutexGuard agiert wie &mut. Mehrere Method-Calls darauf nutzen implizites Reborrowing.
Iterator-State mit peek()
fn parse_zahl<I: Iterator<Item = char>>(iter: &mut std::iter::Peekable<I>) -> Option<u32> {
let mut wert = 0u32;
while let Some(&c) = iter.peek() { // Reborrow für peek
if let Some(d) = c.to_digit(10) {
wert = wert * 10 + d;
iter.next(); // Reborrow für next
} else {
break;
}
}
if wert > 0 { Some(wert) } else { None }
}peek und next auf demselben &mut Peekable<I>. Beide reborrowen implizit. Klassisches Parser-Pattern.
Nested mut-Methode auf Struct-Feld
struct App {
log: String,
counter: u32,
}
impl App {
fn melde(&mut self, msg: &str) {
self.log.push_str(msg);
self.log.push('\n');
self.counter += 1;
}
}
fn main() {
let mut app = App { log: String::new(), counter: 0 };
app.melde("Start"); // Reborrow von app
app.melde("Verarbeite"); // wieder Reborrow
app.melde("Ende");
println!("Counter: {}", app.counter);
}Jeder app.melde(...)-Call reborrowt app. Ohne Reborrowing wäre app nach dem ersten Call weg.
State-Machine mit Reborrow
struct Spiel { score: u32 }
impl Spiel {
fn add(&mut self, n: u32) -> &mut Self {
self.score += n;
self
}
}
fn main() {
let mut s = Spiel { score: 0 };
s.add(10).add(20).add(30); // Method-Chain mit Reborrow
assert_eq!(s.score, 60);
}Diese Variante des Builder-Patterns mit -> &mut Self ist eine elegante Anwendung von Reborrowing. Jede Methode nimmt &mut self, modifiziert das Objekt, und gibt self als &mut Self zurück — was wieder ein mutable Borrow auf dasselbe Objekt ist. Der s.add(10).add(20).add(30)-Aufruf kettet die Borrows nahtlos zusammen.
Im Vergleich zu Buildern, die self per Move nehmen und konsumieren (wie in vielen Construction-APIs), hat diese Variante den Vorteil, dass das Objekt nach der Chain noch existiert — das macht sie für State-Mutations-APIs ideal, bei denen die Identität des Objekts erhalten bleiben soll.
Besonderheiten
Reborrowing macht &mut praktisch verwendbar.
Ohne Reborrowing wäre eine &mut-Referenz nach dem ersten Funktions-Call weg — fast jede idiomatische Mutation würde Move-Probleme verursachen. Reborrowing ist der unsichtbare Mechanismus, der das natürliche Schreiben von Rust-Code ermöglicht.
&mut *r ist die Syntax für expliziten Reborrow.
*r dereferenziert die Referenz — und &mut davor erzeugt eine neue mutable Referenz auf das Ziel. Das Ergebnis ist eine kurzlebige neue Referenz, die mit r ko-existiert (während r „eingefroren" ist).
Funktions-Calls reborrowen automatisch.
Wenn du funktion(r) mit r: &mut T schreibst, fügt der Compiler intern funktion(&mut *r) ein — sofern die Funktion keine Referenz mit der Lifetime von r zurückgibt. Du musst Reborrowing nur sehr selten manuell schreiben.
let r2 = r moved, funktion(r) reborrowt.
Variablen-Zuweisung ist Move. Funktions-Übergabe ist Reborrow. Das ist der wichtigste Merksatz. Wer &mut-Konflikte hat, sollte zuerst prüfen: passiert hier ein Move (Zuweisung) oder ein Reborrow (Call)?
Reborrow funktioniert nur, wenn die Funktion die Lifetime nicht „mitnimmt“.
Wenn fn foo<'a>(r: &'a mut T) -> &'a mut T einen Borrow mit gleicher Lifetime zurückgibt, ist der Aufrufer-r blockiert. Bei fn foo(r: &mut T) (ohne Lifetime-Rückgabe) reborrowt der Compiler frei.
Reborrowing ist nicht „free“ — der Borrow Checker prüft trotzdem.
Während ein Reborrow lebt, ist die Original-Referenz „eingefroren". Du kannst sie nicht parallel zum Reborrow benutzen — nur sequentiell. NLL erkennt aber, dass der Reborrow am Funktions-Ende endet, sodass die Original-Referenz danach frei ist.
Method-Chaining auf &mut Self nutzt Reborrowing.
s.foo().bar().baz() mit Methoden, die &mut self nehmen und &mut Self zurückgeben, ist ein klassisches Reborrowing-Pattern. Jeder Call reborrowt die Self-Referenz für die nächste Methode.
iter_mut() in einer Schleife reborrowt.
for x in v.iter_mut() { ... } reborrowt v für die Dauer des Loops. Nach dem Loop ist v wieder voll verfügbar. Klassisches Idiom, das man oft schreibt, ohne den Mechanismus zu sehen.
Weiterführende Ressourcen
Externe Quellen
- Rust Reference – Reborrow
- The Rustonomicon – Lifetimes
- Rust Internals – Reborrowing
- Niko Matsakis – Reborrows