Ein Struct mit einem Referenz-Feld muss in seiner Definition eine Lifetime deklarieren. Dadurch wird die Struct-Instanz an die Lebenszeit der gehaltenen Referenz gekoppelt: die Instanz darf nicht länger leben als der referenzierte Wert. Dieses Muster ist mächtig für Zero-Copy-Parser, Views, und Cursors über bestehenden Daten — aber es bringt klare Einschränkungen mit sich. Wer es beherrscht, kann sehr effiziente Strukturen bauen; wer die Limits nicht kennt, läuft in Borrow-Checker-Fehler, die schwer zu lösen sind.

Die Grundform

Rust Struct mit einer Referenz
struct Auszug<'a> {
    text: &'a str,
}

fn main() {
    let dokument = String::from("Lorem ipsum dolor");
    let auszug = Auszug { text: &dokument[..5] };
    println!("{}", auszug.text);     // "Lorem"
}

struct Auszug<'a> deklariert einen Lifetime-Parameter 'a. Das Feld text: &'a str nutzt diese Lifetime. Bedeutung: jede Auszug-Instanz hat ein Feld mit einer Referenz, die mindestens 'a lebt — und die Instanz selbst darf nicht länger leben als 'a.

In der Praxis: solange dokument existiert, darf auszug existieren. Wenn dokument dropped wird, muss auszug schon weg sein.

Was die Annotation für die Instanz bedeutet

Rust Lebenszeit-Bindung
struct Auszug<'a> { text: &'a str }

fn main() {
    let auszug;
    {
        let dokument = String::from("Hello World");
        auszug = Auszug { text: &dokument[..5] };
        // auszug lebt mindestens so lange wie dokument — OK hier
    }                              // dokument dropped
    // println!("{}", auszug.text); // FEHLER: text wäre dangling
}

Der Compiler erkennt: auszug.text hat Lifetime 'a, gebunden an dokument. auszug als Ganzes hat damit auch eine Lifetime, die nicht länger sein darf. Nach dem inneren Block ist auszug nicht mehr nutzbar.

Das ist die zentrale Mechanik bei Structs mit Referenzen: die Struct-Instanz erbt die Lifetime ihrer Felder. Sie kann nicht länger leben als das, worauf sie zeigt.

Mehrere Referenz-Felder mit gleicher Lifetime

Rust Mehrere Felder, eine Lifetime
struct PairView<'a> {
    first: &'a str,
    second: &'a str,
}

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world");
    let pair = PairView {
        first: &s1,
        second: &s2,
    };
    println!("{}, {}", pair.first, pair.second);
}

Beide Felder haben die gleiche Lifetime 'a. Die Struct-Instanz lebt höchstens so lange wie der kürzere der beiden referenzierten Werte (s1 oder s2 — je nachdem, welcher zuerst dropped wird).

Mehrere Lifetimes — unabhängige Referenzen

Wenn die Referenzen unabhängige Lebenszeiten haben sollen, deklarierst du mehrere Lifetime-Parameter.

Rust Zwei Lifetimes
struct CrossRef<'src, 'dst> {
    source: &'src str,
    destination: &'dst mut String,
}

impl<'src, 'dst> CrossRef<'src, 'dst> {
    fn copy(&mut self) {
        self.destination.push_str(self.source);
    }
}

fn main() {
    let src = String::from("source");
    let mut dst = String::new();
    {
        let mut cross = CrossRef { source: &src, destination: &mut dst };
        cross.copy();
    }
    println!("{dst}");        // "source"
}

Zwei Lifetimes für zwei semantisch unterschiedliche Referenzen — Source (immutable) und Destination (mutable). Das macht die API flexibler: source und destination können unterschiedliche Lebensspannen haben.

Borrowed vs. Owned

Die Entscheidung „Referenz oder owned Wert?" prägt das Struct-Design fundamental.

Rust Borrowed-Version
struct UserBorrowed<'a> {
    name: &'a str,
    email: &'a str,
}
// Vorteil: keine Allocation, sehr schnell
// Nachteil: Lifetime-Constraint — Instanz an Source-Daten gebunden
Rust Owned-Version
struct UserOwned {
    name: String,
    email: String,
}
// Vorteil: keine Lifetime-Probleme, völlig frei verschiebbar
// Nachteil: Allocation bei jedem Erstellen

Faustregel:

  • Borrowed ist richtig bei kurzlebigen Views/Cursors über vorhandenen Daten — Parser, Tokenizer, Slice-Wrapper.
  • Owned ist richtig bei Domain-Objekten, die unabhängig in Datenbanken, Containern, Threads landen.

Es gibt Hybride: Cow<'a, str> (Copy-on-Write) wechselt zwischen borrowed und owned je nach Bedarf — siehe spätere Kapitel.

Enums mit Referenzen

Enums folgen den gleichen Regeln wie Structs.

Rust Enum mit Ref-Variante
enum Eintrag<'a> {
    Komentar(&'a str),
    Wert(i32),
    Verweis(&'a str, i32),
}

fn main() {
    let kommentar_text = String::from("eine Notiz");
    let eintraege = vec![
        Eintrag::Komentar(&kommentar_text),
        Eintrag::Wert(42),
        Eintrag::Verweis(&kommentar_text, 1),
    ];
    // Eintrag-Instanzen leben nicht länger als kommentar_text
    for e in &eintraege {
        match e {
            Eintrag::Komentar(s) => println!("K: {s}"),
            Eintrag::Wert(n) => println!("V: {n}"),
            Eintrag::Verweis(s, n) => println!("R: {s} → {n}"),
        }
    }
}

Enum mit Lifetime-Parameter: solange irgendeine Variante Referenzen halten könnte, ist der Lifetime-Parameter Pflicht. Die Variantes Wert(i32) enthält keine Referenz — aber sie ist Teil des selben Enum-Typs Eintrag<'a>, der Parameter ist trotzdem nötig.

Praxis: Struct-mit-Lifetime-Pattern

Parser-State

Rust Parser
pub struct Parser<'src> {
    input: &'src str,
    position: usize,
}

impl<'src> Parser<'src> {
    pub fn neu(input: &'src str) -> Self {
        Parser { input, position: 0 }
    }

    pub fn peek(&self) -> Option<char> {
        self.input[self.position..].chars().next()
    }

    pub fn consume(&mut self) -> Option<char> {
        let c = self.peek()?;
        self.position += c.len_utf8();
        Some(c)
    }

    pub fn rest(&self) -> &'src str {
        &self.input[self.position..]
    }
}

fn main() {
    let text = String::from("Hello");
    let mut p = Parser::neu(&text);
    while let Some(c) = p.consume() {
        println!("{c}");
    }
    println!("Rest: {}", p.rest());
}

Zero-Copy-Parser: hält Referenz auf die Source, Position als Cursor. rest() gibt eine Slice mit derselben Lifetime zurück — gebunden an die ursprüngliche Source.

Buffer-View

Rust Buffer-View
pub struct View<'a> {
    data: &'a [u8],
    stride: usize,
}

impl<'a> View<'a> {
    pub fn neu(data: &'a [u8], stride: usize) -> Self {
        View { data, stride }
    }

    pub fn row(&self, idx: usize) -> Option<&'a [u8]> {
        let start = idx * self.stride;
        let end = start + self.stride;
        if end <= self.data.len() {
            Some(&self.data[start..end])
        } else {
            None
        }
    }
}

Matrix-View über einem Byte-Buffer. Zeilen sind Sub-Slices in den Original-Buffer — keine Kopien, alles geliehene Referenzen.

Cursor mit Position

Rust Cursor
pub struct Cursor<'a, T> {
    data: &'a [T],
    position: usize,
}

impl<'a, T> Cursor<'a, T> {
    pub fn neu(data: &'a [T]) -> Self {
        Cursor { data, position: 0 }
    }

    pub fn current(&self) -> Option<&'a T> {
        self.data.get(self.position)
    }

    pub fn advance(&mut self) {
        self.position += 1;
    }
}

fn main() {
    let items = vec![1, 2, 3, 4, 5];
    let mut c = Cursor::neu(&items);
    while let Some(x) = c.current() {
        println!("{x}");
        c.advance();
    }
}

Generischer Cursor — Lifetime und Type-Parameter zusammen.

Hybride mit owned + borrowed

Rust Hybrid
pub struct Request<'a> {
    url: String,                  // owned
    headers: &'a [(&'a str, &'a str)],  // borrowed
    body: Vec<u8>,                // owned
}

impl<'a> Request<'a> {
    pub fn neu(url: String, headers: &'a [(&'a str, &'a str)]) -> Self {
        Request { url, headers, body: Vec::new() }
    }
}

Mischung aus owned und borrowed Feldern. Die owned Felder sind unproblematisch; die borrowed Felder geben dem Struct trotzdem einen Lifetime-Parameter.

Reader mit Backing-Buffer

Rust Reader
pub struct Reader<'buf> {
    buffer: &'buf [u8],
    offset: usize,
}

impl<'buf> Reader<'buf> {
    pub fn neu(buffer: &'buf [u8]) -> Self {
        Reader { buffer, offset: 0 }
    }

    pub fn read_u32(&mut self) -> Option<u32> {
        if self.offset + 4 > self.buffer.len() { return None; }
        let bytes = &self.buffer[self.offset..self.offset + 4];
        self.offset += 4;
        Some(u32::from_le_bytes(bytes.try_into().unwrap()))
    }

    pub fn read_str(&mut self, len: usize) -> Option<&'buf str> {
        if self.offset + len > self.buffer.len() { return None; }
        let slice = &self.buffer[self.offset..self.offset + len];
        self.offset += len;
        std::str::from_utf8(slice).ok()
    }
}

Binary-Reader über einem Byte-Buffer. read_str gibt eine Sub-Slice zurück, die mit der Buffer-Lifetime verknüpft ist.

Builder mit Referenz-Felder

Rust Builder
pub struct QueryBuilder<'a> {
    table: &'a str,
    conditions: Vec<(&'a str, &'a str)>,
}

impl<'a> QueryBuilder<'a> {
    pub fn neu(table: &'a str) -> Self {
        QueryBuilder { table, conditions: Vec::new() }
    }

    pub fn where_eq(mut self, col: &'a str, val: &'a str) -> Self {
        self.conditions.push((col, val));
        self
    }

    pub fn build(&self) -> String {
        let mut q = format!("SELECT * FROM {}", self.table);
        for (i, (col, val)) in self.conditions.iter().enumerate() {
            if i == 0 { q.push_str(" WHERE "); }
            else { q.push_str(" AND "); }
            q.push_str(&format!("{col} = '{val}'"));
        }
        q
    }
}

fn main() {
    let q = QueryBuilder::neu("users")
        .where_eq("name", "Alice")
        .where_eq("city", "Berlin")
        .build();
    println!("{q}");
}

Builder mit Referenz-Strings. Builder lebt nicht länger als die Quellen, die er borgt.

Klassisches Selbst-Referenz-Problem

Rust Self-Ref VERBOTEN
// VERBOTEN — Struct kann sich selbst NICHT referenzieren:
// struct SelfRef<'a> {
//     value: String,
//     pointer: &'a str,    // Soll auf value zeigen
// }
//
// let mut sr = SelfRef { value: String::from("hi"), pointer: ??? };
// sr.pointer = &sr.value;   // FEHLER: borrow checker lehnt ab
//
// Lösung: Indexes statt Referenzen, oder Crate wie `ouroboros`,
// oder Pin + unsafe für fortgeschrittene Fälle.

Selbst-referenzierende Structs sind in Rust nicht direkt möglich. Workaround: Indizes statt Referenzen, oder spezialisierte Crates. Mehr im Advanced-Patterns-Kapitel.

Interessantes

Struct mit Referenz-Feld braucht Lifetime-Parameter.

struct Foo<'a> { r: &'a str } — der Parameter ist Pflicht. Ohne ihn lehnt der Compiler die Definition ab.

Struct-Instanz erbt die Lifetime ihrer Felder.

Die Instanz darf nicht länger leben als der kürzeste der referenzierten Werte. Wenn ein referenzierter Wert dropped wird, ist die Instanz nicht mehr nutzbar.

Mehrere Felder mit gleicher Lifetime — kürzere Quelle wählt.

struct Foo<'a> { a: &'a, b: &'a } — beide gebunden an dieselbe Lifetime. Der Compiler nimmt die kürzere der tatsächlichen Lebenszeiten.

Mehrere Lifetimes für unabhängige Felder.

struct Foo<'src, 'dst> — semantisch unterschiedliche Felder bekommen unterschiedliche Lifetimes. Mehr Flexibilität, aber komplexere Signatur.

Borrowed vs. owned — die zentrale Design-Entscheidung.

Borrowed (mit Lifetime) ist effizient (keine Allocation) aber an Source-Daten gekoppelt. Owned (String, Vec) ist frei verschiebbar, kostet aber Allocation. Cow als Hybrid.

Enums mit Ref-Varianten brauchen auch Lifetime-Parameter.

Selbst wenn nur eine von vielen Varianten Referenzen hat — der Parameter ist Teil des gesamten Enum-Typs.

Selbst-referenzierende Structs sind verboten.

Klassischer Stolperstein. Workaround: Indizes statt Referenzen, oder spezielle Crates (ouroboros, self_cell). Pin + unsafe für sehr fortgeschrittene Fälle.

Impl-Blöcke übernehmen die Lifetime-Parameter.

impl<'a> Foo<'a> { ... } — die Lifetimes müssen im impl-Block deklariert und am Typ aufgeführt werden. Methoden können zusätzliche Lifetimes haben.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Lifetimes

Zur Übersicht