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
}Die wichtigsten Eigenschaften von &T lassen sich in fünf Punkten zusammenfassen, jeweils mit konkreten Konsequenzen für deinen Code.
Beliebig viele parallel auf den gleichen Wert. Zwei, drei, hunderte shared References auf denselben String sind erlaubt — die Begrenzung kommt nicht von Rust, sondern höchstens von der Lebenszeit-Logik. Damit kannst du denselben Wert an viele Funktionen lesend übergeben, ohne ihn duplizieren zu müssen.
Lesen erlaubt, Schreiben verboten — und das gilt auch dann, wenn die ursprüngliche Bindung mut war. Sobald irgendeine &T-Referenz auf den Wert existiert, sind alle Wege gesperrt, ihn zu mutieren, bis der letzte shared Borrow weg ist.
Die Original-Bindung darf parallel auch lesen — der Wert hat ja keinen exklusiven Eigentümer für die Lesezeit. Aber: s.push_str("...") (was intern &mut s braucht) wäre während eines aktiven &s-Borrows blockiert.
Klein im Speicher. Eine &T ist ein 8-Byte-Pointer auf 64-bit-Systemen. Bei Slices (&[T]) und String-Slices (&str) kommen noch 8 Bytes für die Länge hinzu, also 16 Bytes — immer noch billig.
Copy. Referenzen sind triviale Bit-Kopien, sie werden bei Zuweisung dupliziert statt gemoved. Damit kannst du eine Reference frei an mehrere Funktionen weitergeben, ohne über Ownership nachzudenken — die Referenz selbst ist nur ein Lese-Pointer.
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}");
}Bei &mut T herrscht ein anderes Regime: Exklusivität statt Sharing. Drei Voraussetzungen müssen erfüllt sein.
Die Original-Bindung muss mut sein. Eine immutable Bindung kann keine mutable Referenz auf sich selbst herausgeben — das wäre ein Widerspruch zur Zusage „dieser Wert darf nicht verändert werden". Der Compiler prüft das schon syntaktisch.
Die Referenz wird mit dem &mut-Operator erstellt. Es gibt keine implizite Konvertierung von &T zu &mut T — wenn du eine mutable Referenz brauchst, musst du sie ausdrücklich anfordern.
Maximal eine &mut T auf einen Wert zur gleichen Zeit. Diese Exklusivität ist die zentrale Garantie: solange &mut s lebt, gibt es keine andere Referenz auf s — weder eine zweite &mut, noch eine shared &. Diese Regel ist strikt und unverhandelbar — sie ist die Grundlage für Data-Race-Freiheit und Compiler-Optimierungen.
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?
Aliasing-XOR-Mutability klingt zunächst nach einer Schikane — fast alle anderen Sprachen erlauben gleichzeitige Lese- und Schreibzugriffe ohne Beschränkung. Warum ist Rust hier so streng? Drei Gründe, jeder davon allein den Aufwand wert.
Data-Race-Freiheit zur Compile-Zeit. Ein Data Race entsteht, wenn ein Thread schreibt, während ein anderer liest oder schreibt — auf denselben Speicher, ohne Synchronisation. Das ist Undefined Behavior in C/C++ und eine der häufigsten Bug-Quellen in Multi-Thread-Code. Rusts Aliasing-XOR-Mutability eliminiert das per Konstruktion: eine &T kann nicht parallel zu einer &mut T existieren, und das gilt nicht nur innerhalb eines Threads, sondern auch über Thread-Grenzen hinweg (sichergestellt durch die Send/Sync-Traits).
Compiler-Optimierungen. Wenn der Compiler bei einem &T-Parameter weiß, dass der Wert während der Funktion nicht mutiert werden kann (weil sonst eine &mut T parallel existieren müsste, was die Regel verbietet), kann er aggressiv optimieren: Werte in Register cachen, Operationen umsortieren, Methoden inlinen. In C braucht man dafür das restrict-Keyword, das erst seit C99 existiert und selten genutzt wird; in Rust kommt es kostenlos mit dem normalen Typ-System.
Vorhersehbare Mutation. Wer eine &mut T hält, hat eine starke Garantie: kein anderer Code-Pfad sieht den Wert gerade. Du kannst beliebig komplex mutieren, ohne dich um Iterator-Invalidation, gleichzeitige Modifikationen oder Aliasing-Effekte sorgen zu müssen. In C++ ist „while modifying a std::vector, don't iterate over it" eine Disziplin-Frage; in Rust eine Compile-Zeit-Garantie.
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 dieser Code ein Fehler gewesen: r (shared) und v.push(4) (mut) leben formal beide im selben Scope, also gleichzeitig. Mit Non-Lexical-Lifetimes analysiert der Compiler den Code-Fluss: er stellt fest, dass r nach dem println! nicht mehr verwendet wird, und beendet die Borrow-Lebenszeit dort — anstatt am Scope-Ende.
Vor NLL musste man häufig zusätzliche {}-Scope-Blöcke einbauen, um die Lebenszeit von Borrows künstlich zu beschränken. Mit NLL liest sich der Code natürlich von oben nach unten, ohne künstliche Klammern. Das macht idiomatischen Rust drastisch einfacher und ist eine der wichtigsten Quality-of-Life-Verbesserungen der letzten Jahre.
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}");
}Diese drei Modi decken praktisch alle Funktions-Signaturen ab, die du jemals schreiben wirst. Die Faustregel lautet: &T wenn die Funktion nur liest, &mut T wenn sie schreibt, T wenn sie übernimmt. Die Wahl ist nicht beliebig — sie kommuniziert dem Aufrufer (und dem Compiler), was genau passiert.
Je niedriger der „Eingriff", desto besser die API: &T ist der defensivste Modus (kein Wechsel, kein Mutationsrisiko), gefolgt von &mut T (Mutation, aber kein Verlust), und schließlich T (kompletter Übergang). Wer eine API entwirft, sollte zuerst überlegen: „kann ich mit &T arbeiten?". Wenn ja, ist das fast immer die richtige Wahl.
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
}Die beiden Validator-Funktionen demonstrieren, wie idiomatische Rust-Libraries Read-Only-APIs aufbauen. &str als Parameter akzeptiert sowohl String-Literale ("foo@example.com"), als auch &String-Borrows, als auch sub-Slices — der Aufrufer hat maximale Flexibilität, ohne dass Allocations entstehen.
Bemerkenswert ist die zweite Funktion: sie gibt einen Vec<&'static str> zurück. Die 'static-Lifetime der inneren Strings funktioniert, weil alle Push-Argumente String-Literale sind, die im Read-Only-Datensegment des Binaries leben und ewig gültig sind. Damit muss der Aufrufer keine Lifetime-Bindung an den Input beachten — die Liste lebt unabhängig vom Passwort.
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()
}
}Die &self-Methoden des UserStore sind read-only — sie können nichts verändern. Der Rückgabewert ist eine &String-Referenz in die Map hinein, kein Klon. Das bedeutet: solange der Aufrufer die Referenz hält, ist der Store für mutierende Operationen gesperrt (Aliasing-XOR-Mutability). Aber: er kann beliebig viele finden-Calls parallel machen, alles ist shared.
Das ist eine sehr typische API-Form: alle Lese-Operationen sind &self, alle schreibenden sind &mut self. Aufrufer können die &self-Methoden in beliebigem Multi-Reader-Pattern verwenden, etwa aus mehreren Threads gleichzeitig (mit zusätzlichem Sync-Trait); schreibende Operationen brauchen exklusiven Zugriff.
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 die mutierenden Methoden. Während eines dieser Calls hat der Compiler eine Exklusivitäts-Garantie: niemand sonst (kein anderer Thread, kein anderer Aufrufer) hat eine Referenz auf den Store. Das macht die Implementation der Methoden einfach — sie müssen sich nicht um gleichzeitige Modifikationen sorgen.
Der loeschen-Rückgabewert zeigt einen schönen Aspekt der HashMap-API: remove gibt den entfernten Wert zurück (oder None, wenn der Key nicht da war). So kannst du in einer Operation Lookup und Remove kombinieren — wenn der Eintrag fehlt, weißt du es sofort, ohne separates contains_key vorher.
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]);
}Hier siehst du eine Ausnahme von „nur ein mutable Borrow gleichzeitig": split_at_mut ist eine Stdlib-Methode, die einen Slice in zwei mutable Sub-Slices teilt. Diese sind garantiert disjunkt — die linken Bytes und die rechten Bytes überlappen sich nicht. Damit kann der Compiler sicher sein, dass Modifikationen am links-Slice die Bytes im rechts-Slice nicht beeinflussen.
Diese Spezial-Methoden gibt es überall, wo das Aliasing-XOR-Mutability-Prinzip zu restriktiv wäre. Bei normalen Vec-Indices v[i] kennt der Compiler keine Garantie, ob zwei Indices verschiedene Elemente meinen — er muss konservativ ablehnen. Die expliziten Split-Methoden machen die Disjunktheit zur API-Garantie und ermöglichen so legalen Multi-Mutable-Borrow-Code.
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 })
}Zero-Copy-Parsing ist ein Schlüssel zu Performance-kritischem Rust-Code. Statt für jedes geparste Feld einen neuen String zu allozieren, hält der Header-Struct nur Referenzen in den ursprünglichen Input-Buffer. Bei tausenden Zeilen pro Datei sparst du damit tausende von Allocations.
Der Lifetime-Parameter <'a> ist die Compile-Zeit-Garantie: der Header lebt höchstens so lange wie der Input-String. Wenn der Aufrufer den Input freigibt, ist der Header invalid — und der Compiler verhindert solche Use-after-Free-Bugs. Im Lifetimes-Kapitel werden wir das im Detail ansehen; hier siehst du nur, dass die Mechanik existiert und natürlich aus der Borrow-Mechanik folgt.
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 — die Wahl hängt vom Anwendungskontext ab. Der funktionale Stil produziert einen neuen Vec, lässt das Original unangetastet und ist semantisch klarer; er funktioniert ohne mutable Bindung und ist gut, wenn das Original auch nach dem Aufruf gebraucht wird. Der mutierende Stil spart die Allocation des neuen Vec, modifiziert direkt im Speicher des Originals und ist die schnellere Variante bei sehr großen Datenmengen.
Eine pragmatische Faustregel: für Bibliothek-APIs und Default-Cases den funktionalen Stil; für Performance-kritischen Inner-Loop-Code den mutierenden. Manchmal lohnt es sich, beide Varianten anzubieten — die Stdlib selbst macht das oft, etwa mit to_uppercase() (allokiert) vs. make_ascii_uppercase() (in-place).
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 eine Option<&[u8]> zurück — eine Referenz in die Cache-Map hinein. Der Aufrufer kann mit den Daten arbeiten, ohne dass die Map geklont oder verändert wird. Solange er die Referenz hält, kann der Cache nicht mutiert werden — was Sinn macht, weil eine gleichzeitige Mutation den Borrow invalidieren könnte (z. B. wenn die HashMap reallociert).
Das .map(|v| v.as_slice()) ist eine subtile, aber wichtige Konvertierung: get liefert Option<&Vec<u8>>, wir wollen aber Option<&[u8]>. Über as_slice() wird die Vec-Referenz zu einer Slice-Referenz — semantisch gleich, aber für den Aufrufer flexibler. Er sieht nur einen &[u8] und muss nicht wissen, dass intern ein Vec liegt.
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? })
}
}Die Config<'a>-Struktur ist ein Beispiel für „borrowed Configuration" — sie hält Referenzen in den ursprünglichen Input-String, ohne irgendetwas zu allozieren. Der Lifetime-Parameter 'a bindet den Config an die Lebenszeit des Inputs: solange der Input lebt, ist der Config gültig.
Dieses Pattern ist besonders sinnvoll bei Parsern für große Konfigurations- oder Datendateien, wo Allocations massiv ins Gewicht fallen würden. Der Trade-off: der Aufrufer muss den Input-String am Leben halten, solange er den Config verwendet — was bei einer in main geladenen Datei selten ein Problem ist, bei kurzlebigen Strings aber zur Falle werden kann. Wenn diese Disziplin schwierig wird, wechselt man zur Variante mit owned Strings.
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