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:

Rust Shared Reference
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 mut war.
  • 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

Rust Schreiben durch &T verboten
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:

Rust Mutable Reference
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 mut sein.
  • Die Referenz wird mit &mut erstellt.
  • Maximal eine &mut T auf einen Wert zur gleichen Zeit — und keine andere Referenz parallel.

Exklusivität

Rust Exklusivität — strikt
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:

Rust rustc-Diagnose
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

Der 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 &T aktive Referenzen, kein &mut T.
  • Mutability: genau eine &mut T, kein &T und kein zweites &mut T.
Rust Konflikt-Beispiel
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 &T kann nicht parallel zu einem &mut T existieren, 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 ohne restrict-Keyword schwierig.
  • Vorhersehbare Mutation. Wer einen &mut T hä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.

Rust Mit NLL — funktioniert
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

Rust Erstellen
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):

Rust Auto-Deref
let s = String::from("Hi");
let r = &s;
println!("{}", r.len());          // automatisch: r.len() = (*r).len()
println!("{r}");                  // println! arbeitet direkt auf Referenz

Schreiben durch eine &mut-Referenz

Methoden mit &mut self lassen sich direkt aufrufen:

Rust Mutation
let mut s = String::from("Hi");
let r = &mut s;
r.push_str(", Welt");          // Methode mit &mut self

Direkt-Mutation der Pointee braucht *:

Rust Direkt-Mutation
let mut x = 5;
let r = &mut x;
*r += 1;                       // x ist jetzt 6
*r = 100;                       // x ist jetzt 100

References als Funktions-Parameter

Die häufigste Nutzung:

Rust Drei Parameter-Modi
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:

Rust &T ist Copy
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:

Rust &mut T nicht Copy
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

Rust Iter + Push verboten
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

Rust Verschachtelte Borrows
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

Rust Split-Borrow
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

Rust Move während Borrow
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

Rust Validator
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

Rust Service
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

Rust Mutator
# 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

Rust Slice-Split
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

Rust Zero-Copy-Parser
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

Rust Zwei Stile
// 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

Rust Cache
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

Rust Borrowed Config
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

/ Weiter

Zurück zu References & Borrowing

Zur Übersicht