Rust hat genau zwei Reference-Typen: &T und &mut T. Diese Schlichtheit verbirgt eine extrem mächtige Garantie: zu jeder Zeit gibt es entweder beliebig viele lesende Referenzen ODER genau eine schreibende Referenz auf einen Wert — niemals beides. Diese Aliasing-XOR-Mutability-Regel ist das Fundament für Data-Race-Freiheit, Compiler-Optimierungen und eine ganze Klasse von eliminierten Bugs. Dieser Artikel zeigt beide Reference-Typen im Detail, ihre exakten Regeln, die wichtigsten Borrow-Operationen und die Stolperfallen, die jeder Einsteiger kennenlernen muss.
&T — die shared Reference
&T (gesprochen „shared reference auf T") ist eine lesende, geteilte Referenz:
fn main() {
let s = String::from("Hallo");
let r1: &String = &s;
let r2: &String = &s;
let r3: &String = &s;
// Drei shared References auf denselben Wert — alles ok.
println!("{r1} {r2} {r3}");
println!("{s}"); // Original-Bindung weiterhin nutzbar
}Eigenschaften:
- Beliebig viele parallel auf den gleichen Wert.
- Lesen erlaubt, Schreiben verboten — selbst wenn die Original-Bindung
mutwar. - Die Original-Bindung darf gleichzeitig auch lesen, nicht aber schreiben.
- Klein im Speicher: 8 Bytes auf 64-bit (ein Pointer).
Copy— Referenzen sind triviale Bit-Kopien.
Was eine &T-Referenz nicht darf
fn main() {
let mut s = String::from("Hi");
let r: &String = &s;
// r.push_str("!"); // Fehler — &T erlaubt nur lesen
// *r = String::from("Neu"); // Fehler — kein Schreiben durch &T
println!("{r}");
}Auch wenn s mut ist: eine &String-Referenz auf s erlaubt nur lesende Methoden. Mutation würde die Aliasing-XOR-Mutability-Regel brechen.
&mut T — die exklusive Reference
&mut T (gesprochen „mutable reference auf T") ist eine schreibende, exklusive Referenz:
fn main() {
let mut s = String::from("Hallo");
let r: &mut String = &mut s;
r.push_str(", Welt!");
println!("{r}"); // "Hallo, Welt!"
// s wird hier wieder direkt nutzbar, sobald r „tot" ist
println!("{s}");
}Voraussetzungen:
- Die Original-Bindung muss
mutsein. - Die Referenz wird mit
&muterstellt. - Maximal eine
&mut Tauf einen Wert zur gleichen Zeit — und keine andere Referenz parallel.
Exklusivität
fn main() {
let mut s = String::from("Hi");
let r1 = &mut s;
// let r2 = &mut s; // Fehler — zwei mutable Refs auf s
// let r3 = &s; // Fehler — &mut und & gleichzeitig
r1.push_str("!");
println!("{r1}");
}Compiler-Fehler bei dem Versuch, eine zweite Referenz zu erzeugen:
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 hereDer Compiler zeigt exakt: erster Borrow, zweiter Borrow, spätere Verwendung des ersten. Damit lässt sich der Konflikt sofort lokalisieren.
Die zentrale Regel — Aliasing XOR Mutability
Formal: zu jeder Zeit gilt für jeden Wert genau eines:
- Aliasing: beliebig viele
&Taktive Referenzen, kein&mut T. - Mutability: genau eine
&mut T, kein&Tund kein zweites&mut T.
fn main() {
let mut s = String::from("Hi");
let r1 = &s; // shared
let r2 = &s; // shared — ok, parallel zu r1
// let r3 = &mut s; // Fehler — mut neben shared
println!("{r1} {r2}");
}Während r1 und r2 als shared Refs leben, ist KEINE mutable Ref auf s erlaubt — auch nicht eine Direkt-Mutation von s. Erst wenn die letzten shared Refs „tot" sind (nicht mehr verwendet), kann mut wieder aktiv werden.
Warum diese Regel?
Drei Gründe — jeder davon allein wäre den Aufwand wert:
- Data-Race-Freiheit zur Compile-Zeit. Ein Data Race ist Lesen + Schreiben gleichzeitig auf denselben Speicher von verschiedenen Threads. Aliasing-XOR-Mutability eliminiert das per Konstruktion — ein
&Tkann nicht parallel zu einem&mut Texistieren, also auch nicht über Thread-Grenzen. - Compiler-Optimierungen. Bei einem
&T-Parameter darf der Compiler annehmen, dass der Wert während der Funktion nicht mutiert. Damit kann er aggressiv inlinen, Loads cachen, Operationen umordnen. In C ist das ohnerestrict-Keyword schwierig. - Vorhersehbare Mutation. Wer einen
&mut Thält, weiß: niemand anderes hat eine Referenz auf den Wert. Mutation kann beliebig komplex sein, ohne Aliasing-Probleme.
NLL — Non-Lexical Lifetimes
Seit Edition 2018 verfolgt der Borrow Checker References nicht mehr lexikalisch (Scope-basiert), sondern flow-sensitive. Eine Referenz lebt nur bis zu ihrer letzten Verwendung, nicht bis zum Scope-Ende.
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);
}Vor NLL wäre das ein Fehler gewesen: r (shared) und push (mut) im gleichen Scope. Mit NLL erkennt der Compiler, dass r nach println! nicht mehr verwendet wird — die Borrow-Lebenszeit endet dort.
Das macht idiomatischen Rust-Code drastisch einfacher.
Reference-Operationen
Erstellen einer Referenz
let mut s = String::from("Hi");
let r1: &String = &s; // & für shared
let r2: &mut String = &mut s; // (nur wenn r1 nicht parallel lebt!)Lesen durch eine Referenz
Bei der meisten APIs wird das * für Dereferenzierung automatisch eingefügt (Auto-Deref, siehe eigener Artikel):
let s = String::from("Hi");
let r = &s;
println!("{}", r.len()); // automatisch: r.len() = (*r).len()
println!("{r}"); // println! arbeitet direkt auf ReferenzSchreiben durch eine &mut-Referenz
Methoden mit &mut self lassen sich direkt aufrufen:
let mut s = String::from("Hi");
let r = &mut s;
r.push_str(", Welt"); // Methode mit &mut selfDirekt-Mutation der Pointee braucht *:
let mut x = 5;
let r = &mut x;
*r += 1; // x ist jetzt 6
*r = 100; // x ist jetzt 100References als Funktions-Parameter
Die häufigste Nutzung:
fn lesen(s: &String) -> usize {
s.len() // & für shared Lesen
}
fn anhaengen(s: &mut String, suffix: &str) {
s.push_str(suffix); // &mut für Mutation
}
fn nehmen(s: String) -> usize {
s.len()
} // s wird hier gedroppt — Aufrufer hat ihn nicht mehr
fn main() {
let mut text = String::from("Hi");
let n = lesen(&text); // text bleibt nutzbar
anhaengen(&mut text, "!"); // text wird mutiert
let n2 = nehmen(text); // text gemoved
// text danach nicht mehr nutzbar
println!("{n} {n2}");
}Faustregel: &T wenn die Funktion nur liest, &mut T wenn sie schreibt, T wenn sie übernimmt.
References sind Copy
Eine wichtige Eigenheit: &T ist Copy. Du kannst eine Reference frei kopieren, ohne Move-Probleme:
fn main() {
let s = String::from("Hi");
let r1 = &s;
let r2 = r1; // Kopie der Referenz (8 Bytes)
let r3 = r1; // wieder Kopie — r1 weiterhin nutzbar
println!("{r1} {r2} {r3}");
}&mut T ist hingegen nicht Copy — Exklusivität würde sonst gebrochen:
fn main() {
let mut s = String::from("Hi");
let r1 = &mut s;
// let r2 = r1; // Fehler — &mut T ist nicht Copy
// (außer in Reborrow-Kontexten, siehe eigener Artikel)
r1.push_str("!");
}Klassische Stolperfallen
Iteration während Mutation
fn main() {
let mut v = vec![1, 2, 3];
for x in &v { // shared Borrow auf v
if *x > 1 {
// v.push(99); // Fehler — v.push braucht &mut v
}
}
}Während eines for-Loops über &v ist v shared geborrowt. push braucht &mut v — Konflikt. Lösung: Indizes sammeln und nach dem Loop modifizieren, oder iter_mut() für direkte Mutation.
Reference-Konflikt mit Methoden-Calls
fn main() {
let mut v = vec![1, 2, 3];
// v.push(v.len() as i32); // Fehler — &v (len) und &mut v (push) parallel
let len = v.len() as i32;
v.push(len); // ok
}Der Aufruf v.push(v.len()) braucht gleichzeitig einen &v (für len) und einen &mut v (für push). Lösung: len vorher extrahieren.
Mehrere &mut über verschiedene Felder eines Structs
struct Punkt { x: i32, y: i32 }
fn main() {
let mut p = Punkt { x: 0, y: 0 };
let rx = &mut p.x;
let ry = &mut p.y; // ok! verschiedene Felder
*rx = 10;
*ry = 20;
println!("{} {}", p.x, p.y);
}Der Borrow Checker erkennt disjunkte Felder — zwei mutable Refs auf verschiedene Felder desselben Structs sind erlaubt. Bei Indices in einem Vec funktioniert das nicht — dafür gibt es split_at_mut.
Borrowed Wert kann nicht gemoved werden
fn drucken(s: &String) { println!("{s}"); }
fn nehmen(s: String) {}
fn main() {
let s = String::from("Hi");
let r = &s;
// nehmen(s); // Fehler — s ist geborgt
drucken(r);
nehmen(s); // ok — r nicht mehr in Verwendung
}Solange eine Referenz auf s lebt, kann s nicht gemoved werden. Sonst hätte die Referenz einen Pointer auf nichts — Dangling Reference.
Praxis: References im echten Code
Read-Only-Library-API
pub fn ist_gueltige_email(s: &str) -> bool {
s.contains('@') && !s.starts_with('@') && !s.ends_with('@')
}
pub fn finde_passwort_schwaechen(passwort: &str) -> Vec<&'static str> {
let mut probleme = Vec::new();
if passwort.len() < 12 { probleme.push("zu kurz"); }
if !passwort.chars().any(|c| c.is_ascii_digit()) {
probleme.push("keine Ziffer");
}
if !passwort.chars().any(|c| c.is_uppercase()) {
probleme.push("kein Großbuchstabe");
}
probleme
}&str-Parameter — keine Allocation beim Aufrufer, kein Move. Die Funktionen sind frei kombinierbar.
Service-Methode mit &self
use std::collections::HashMap;
pub struct UserStore {
users: HashMap<u64, String>,
}
impl UserStore {
pub fn finden(&self, id: u64) -> Option<&String> {
self.users.get(&id)
}
pub fn alle_namen(&self) -> Vec<&String> {
self.users.values().collect()
}
}&self-Methoden geben &String zurück — keine Klone, kein Move. Aufrufer kann mehrere finden-Calls parallel machen (alle shared).
Mutating Methode mit &mut self
# use std::collections::HashMap;
# pub struct UserStore { users: HashMap<u64, String> }
impl UserStore {
pub fn hinzufuegen(&mut self, id: u64, name: String) {
self.users.insert(id, name);
}
pub fn loeschen(&mut self, id: u64) -> Option<String> {
self.users.remove(&id)
}
}&mut self für Mutation. Solange hinzufuegen läuft, gibt es keine andere Referenz auf den Store — Thread-Safety per Konstruktion.
Buffer-Splitting mit split_at_mut
fn invertiere_haelften(buffer: &mut [u8]) {
let mid = buffer.len() / 2;
let (links, rechts) = buffer.split_at_mut(mid);
// links und rechts sind zwei DISJUNKTE &mut [u8]
links.reverse();
rechts.reverse();
}
fn main() {
let mut daten = [1u8, 2, 3, 4, 5, 6];
invertiere_haelften(&mut daten);
assert_eq!(daten, [3, 2, 1, 6, 5, 4]);
}split_at_mut ist die idiomatische Lösung für „ich brauche zwei mutable Borrows in dieselbe Sammlung" — sie teilt den Slice in zwei garantiert disjunkte Hälften.
Parser mit Borrow auf den Input-Buffer
pub struct Header<'a> {
pub name: &'a str,
pub wert: &'a str,
}
pub fn parse_header(zeile: &str) -> Option<Header<'_>> {
let pos = zeile.find(':')?;
let name = zeile[..pos].trim();
let wert = zeile[pos + 1..].trim();
Some(Header { name, wert })
}Der Header-Struct hält Referenzen in den Input-Buffer — keine Allocation, kein Copy. Solange der Original-String lebt, ist der Header gültig. Lifetime-Parameter ('a) sind hier sichtbar; im Lifetimes-Kapitel mehr dazu.
Funktional vs. mutierend
// Funktional: &T rein, owned raus
pub fn quadrate(zahlen: &[i32]) -> Vec<i32> {
zahlen.iter().map(|&n| n * n).collect()
}
// Mutierend: &mut T, gibt nichts zurück
pub fn quadriere_in_place(zahlen: &mut [i32]) {
for n in zahlen.iter_mut() {
*n = *n * *n;
}
}
fn main() {
let v = vec![1, 2, 3, 4];
let q = quadrate(&v);
assert_eq!(q, vec![1, 4, 9, 16]);
let mut m = vec![1, 2, 3, 4];
quadriere_in_place(&mut m);
assert_eq!(m, vec![1, 4, 9, 16]);
}Beide Stile sind valide. Funktional ist sicherer (Original bleibt), mutierend ist potentiell schneller (keine Allocation).
Cache-Lookup mit Borrow-Rückgabe
use std::collections::HashMap;
pub struct Cache {
eintraege: HashMap<String, Vec<u8>>,
}
impl Cache {
pub fn holen(&self, key: &str) -> Option<&[u8]> {
self.eintraege.get(key).map(|v| v.as_slice())
}
}
fn main() {
let mut c = Cache { eintraege: HashMap::new() };
c.eintraege.insert("foo".into(), vec![1, 2, 3]);
if let Some(daten) = c.holen("foo") {
println!("{daten:?}"); // [1, 2, 3]
}
}holen gibt Option<&[u8]> zurück — die Daten leben in der Cache-Map, der Aufrufer borrowt nur. Kein Klon, kein Move.
Configuration mit &str-Lifetimes
pub struct Config<'a> {
pub host: &'a str,
pub port: u16,
}
impl<'a> Config<'a> {
pub fn aus_string(input: &'a str) -> Option<Config<'a>> {
let mut host = None;
let mut port = None;
for line in input.lines() {
if let Some(v) = line.strip_prefix("host=") { host = Some(v); }
if let Some(v) = line.strip_prefix("port=") { port = v.parse().ok(); }
}
Some(Config { host: host?, port: port? })
}
}Der Config-Struct lebt nicht länger als sein Input-String. Damit sind alle Felder zero-copy — perfekt für Performance-kritische Parser.
Häufige Stolperfallen
Aliasing-XOR-Mutability ist die EINE Regel.
Fast jeder Borrow-Checker-Fehler reduziert sich auf diese Regel: gibt es gerade &mut T neben &T (oder einem anderen &mut T)? Wenn ja, kompiliert es nicht. Wer diesen mentalen Test verinnerlicht hat, debuggt Borrow-Fehler in Sekunden.
Ein Borrow lebt bis zur letzten Verwendung.
Dank Non-Lexical-Lifetimes (NLL) endet ein Borrow nicht am Scope-Ende, sondern an seiner letzten Verwendung im Code-Pfad. Viele „Konflikte" lösen sich auf, wenn du den letzten println!-Aufruf vor den &mut-Aufruf ziehst.
&mut T braucht let mut x.
Wer &mut x schreibt, muss let mut x = ...; deklariert haben. Der Compiler verlangt diese explizite Erklärung des Aufrufers, dass sein Wert mutierbar sein darf.
&T ist Copy, &mut T nicht.
Shared References sind triviale 8-Byte-Werte — Copy. Mutable References dürfen es nicht sein, sonst hätten plötzlich zwei Code-Stellen mutable Zugriff. Beim Übergeben einer &mut-Ref an eine Funktion entsteht aber kein Move — Reborrowing kommt zum Einsatz (eigener Artikel).
Strukturen mit Referenz-Feldern brauchen Lifetimes.
struct A { r: &i32 } ist Compile-Fehler — der Compiler weiß nicht, wie lange r leben soll. Lösung: explizites Lifetime-Parameter struct A<'a> { r: &'a i32 }. Mehr im Lifetimes-Kapitel.
Borrow Checker kennt disjunkte Felder.
Zwei mutable Refs auf unterschiedliche Felder eines Structs sind erlaubt: let rx = &mut p.x; let ry = &mut p.y; funktioniert. Bei Vec-Indices NICHT — der Borrow Checker kann v[0] und v[1] nicht als disjunkt erkennen. Dafür gibt es split_at_mut, iter_mut().enumerate() oder das slice::split_at_mut-Pattern.
Method-Calls fügen implizit Borrow ein.
s.len() ist syntaktischer Zucker für String::len(&s). Methoden mit &self-Receiver erzeugen einen Shared Borrow für die Dauer des Calls; &mut self einen Mutable Borrow. Das macht den Borrow-Check oft unsichtbar — aber die Regeln gelten.
iter() liefert &T, iter_mut() liefert &mut T, into_iter() liefert T.
Drei Iterations-Modi. iter() für reines Lesen (shared Refs), iter_mut() für In-Place-Modifikation (mut Refs), into_iter() für verbrauchende Iteration (Move). Wahl bestimmt, ob die Sammlung danach noch existiert.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – References and Borrowing
- Rust Reference – References
- The Rustonomicon – Aliasing
- rustc Error E0499
- rustc Error E0502
- Non-Lexical Lifetimes RFC