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:
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: der Compiler fügt diese Reborrow-Operation automatisch ein, wenn du eine &mut-Referenz an eine Funktion übergibst.
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}");
}Jeder Method-Call auf &mut self reborrowt implizit. Ohne Reborrowing wäre r nach dem ersten Aufruf weg.
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:?}");
}Der buffer-Parameter wird in der Schleife jedes Mal an verarbeite_einen reborrowt. Nach dem Call ist buffer wieder voll verfügbar.
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
}Drei sequentielle execute-Calls auf &mut Connection. Ohne Reborrowing wäre nach dem ersten Call die Connection „verbraucht" — Reborrowing macht die natürliche Sequenz möglich.
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);
}-> &mut Self ermöglicht Method-Chaining mit Self-Return. Jeder Call reborrowt das eingehende &mut self für die nächste Methode.
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