Eine Dangling Reference ist ein Pointer, der auf Speicher zeigt, der schon freigegeben wurde. In C/C++ entstehen sie ständig — der Pointer überlebt das Objekt, das er anvisiert, und ein späterer Zugriff liest Müll oder crasht. In Safe Rust sind sie per Konstruktion unmöglich: der Compiler beweist zur Bauzeit, dass jede Referenz nur so lange existiert wie ihr Ziel. Das Werkzeug dafür sind Lifetimes — und dieser Artikel ist ihre erste, sanfte Einführung. Wir zeigen, was eine Dangling Reference wäre, wie der Compiler sie ablehnt, lesen die wichtige E0597-Diagnose und legen die Grundlage für das spätere Lifetimes-Kapitel.

Was eine Dangling Reference ist

In C sieht das so aus:

Rust C — klassischer Bug
int* erstelle_zahl() {
    int x = 42;
    return &x;          // Pointer auf x — aber x lebt nur in dieser Funktion!
}   // x wird hier freigegeben.

int main() {
    int* p = erstelle_zahl();
    printf("%d\n", *p);   // Undefined Behavior — liest aus freigegebenem Speicher
}

Der Pointer p zeigt auf eine Variable, die nicht mehr existiert. Das Programm crasht — oder liest seltsamen Müll. Eine ganze Klasse von Sicherheitslücken (CVE-Datenbank ist voll davon).

In Rust ist exakt das Compile-Fehler:

Rust Rust — verboten
fn erstelle_zahl() -> &i32 {
    let x = 42;
    &x                   // Fehler — x lebt nicht lang genug
}

Diagnose:

Rust rustc-Diagnose
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:23
  |
1 | fn erstelle_zahl() -> &i32 {
  |                       ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there
          is no value for it to be borrowed from

Schon die Signatur wird abgelehnt — der Compiler erkennt, dass die Funktion keine Quelle für die zurückgegebene Referenz hat. Die Funktion müsste den Wert by-value zurückgeben oder einen Lifetime-Parameter haben, an dem die Rückgabe hängt.

E0597 — borrowed value does not live long enough

Der häufigste Lifetime-Fehler. Eine Referenz lebt länger als ihr Ziel:

Rust E0597-Beispiel
fn main() {
    let r;
    {
        let s = String::from("temporär");
        r = &s;             // s lebt nur im inneren Block
    }   // s wird hier gedroppt
    println!("{r}");        // E0597 — r zeigt ins Nichts
}

Diagnose:

Rust rustc-Diagnose
error[E0597]: `s` does not live long enough
 --> src/main.rs:5:17
  |
5 |             r = &s;
  |                 ^^ borrowed value does not live long enough
6 |         }
  |         - `s` dropped here while still borrowed
7 |         println!("{r}");
  |                   --- borrow later used here

Der Compiler zeigt exakt:

  • Wo geborgt wurde (r = &s).
  • Wo das Ziel gedroppt wird (} nach dem Block).
  • Wo die Referenz später noch genutzt wird (println!).

Das Problem: s lebt nur bis zum Ende des inneren Blocks. r würde danach auf freigegebenen Speicher zeigen — der Compiler lehnt es ab.

Behebung

Rust Fix
fn main() {
    let s = String::from("permanenter");      // s lebt im Funktions-Scope
    let r = &s;
    println!("{r}");                          // ok
}

Wenn s mindestens so lange lebt wie r, ist alles in Ordnung.

Lifetimes als Compile-Zeit-Verfolgung

Hinter den Kulissen verfolgt der Compiler für jede Referenz eine Lifetime — die Zeitspanne, in der sie gültig ist. Lifetimes sind keine Laufzeit-Werte; sie sind ein Compile-Zeit-Konzept. Du siehst sie typischerweise als Buchstaben mit Apostroph: 'a, 'static, 'src.

In den meisten Fällen schließt der Compiler Lifetimes implizit (Lifetime Elision). Du musst sie nur dann explizit schreiben, wenn er sie nicht eindeutig herleiten kann.

Rust Implizit (Elision)
fn ersten_chars(s: &str) -> &str {
    &s[..5]
}
// Implizit: fn ersten_chars<'a>(s: &'a str) -> &'a str
// Bedeutung: die Rückgabe lebt mindestens so lange wie der Input.
Rust Explizit nötig
fn laengeres<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
// Hier muss der Programmierer die Lifetime explizit verbinden:
// beide Eingaben und die Rückgabe müssen die gleiche Lifetime haben.

Bei der Funktion laengeres weiß der Compiler nicht, ob das Resultat aus a oder aus b kommt. Die Lifetime-Annotation 'a sagt: „beide Eingaben leben mindestens so lange 'a, die Rückgabe auch". Damit kann der Compiler sicherstellen, dass kein Dangling entsteht.

Lifetimes sind ein eigenes Kapitel — hier reicht das mentale Modell: Lifetimes sind das Compile-Zeit-Werkzeug, das Dangling References verhindert.

Klassische Dangling-Versuche

Reference auf lokale Variable zurückgeben

Rust Verbot
// Geht NICHT:
// fn schlecht() -> &String {
//     let s = String::from("Hi");
//     &s            // s wird gleich gedroppt — Referenz wäre dangling
// }

// Geht — String by-value zurückgeben:
fn gut() -> String {
    String::from("Hi")
}

Eine lokale Variable kann nicht per Referenz nach draußen geleakt werden. Lösung: Wert by-value zurückgeben (Move).

Reference in Struct ohne Lifetime

Rust Struct-Lifetime
// Geht NICHT:
// struct Wrapper { text: &str }

// Geht — mit Lifetime-Parameter:
struct Wrapper<'a> {
    text: &'a str,
}

fn main() {
    let s = String::from("Hi");
    let w = Wrapper { text: &s };
    println!("{}", w.text);
}

Ein Struct, der Referenzen hält, braucht einen Lifetime-Parameter. Damit kann der Compiler prüfen, dass die Struct-Instanz nicht länger lebt als das Ziel der Referenz.

Reference auf temporäre Expression

Rust Temporary lifetime
fn main() {
    // let r = &String::from("Hi");
    // ^ wäre Compile-Fehler im allgemeinen Fall;
    // ein „Temporary Lifetime Extension"-Mechanismus rettet es manchmal.

    // Robuster und expliziter:
    let s = String::from("Hi");
    let r = &s;
    println!("{r}");
}

Rust hat einen Mechanismus namens Temporary Lifetime Extension, der in bestimmten Fällen die Lebenszeit eines Temporary-Wertes verlängert, sodass eine Referenz darauf gültig bleibt. Aber: Verlass dich nicht darauf in komplexen Patterns.

Warum dieser Compile-Zeit-Beweis möglich ist

Drei Bausteine machen die Dangling-Prevention möglich:

  • Ownership — jeder Wert hat einen klaren Besitzer, der seine Lebenszeit definiert.
  • Borrow Checker — verfolgt jede Referenz und ihre Lebenszeit.
  • Lifetime-Annotationen — geben dem Compiler die nötige Information, wo Inferenz nicht reicht.

Zusammen ergibt das eine mathematisch beweisbare Garantie: jede Referenz in einem kompilierten Programm zeigt auf gültigen Speicher. Keine Use-after-Free-Bugs in Safe Rust.

Was 'static bedeutet

Die spezielle Lifetime 'static markiert Werte, die für die gesamte Programm-Laufzeit leben:

Rust 'static
// String-Literale haben 'static:
let s: &'static str = "Hi";

// const und static-Items auch:
const HOST: &'static str = "localhost";
static MAX: u32 = 100;

// 'static lifetime von Box::leak:
fn aus_leak() -> &'static str {
    let s = String::from("dynamisch erzeugt");
    Box::leak(s.into_boxed_str())
}

'static heißt nicht „statisch im Binary" — es heißt „lebt mindestens so lange wie das Programm". Funktionen wie thread::spawn verlangen ihre Closure-Captures als 'static, weil ein Thread potentiell länger leben kann als der Aufrufer.

Praxis: Dangling-Prevention im echten Code

Funktion gibt owned Wert zurück

Rust Owned-Return
pub fn baue_pfad(host: &str, ressource: &str) -> String {
    format!("https://{host}/{ressource}")
}

fn main() {
    let url = baue_pfad("example.com", "users/42");
    println!("{url}");
}

Wenn die Funktion einen neuen Wert produziert, gibt sie ihn by-value zurück — kein Dangling-Risiko.

Funktion borrowt und gibt Sub-Slice zurück

Rust Slice-Return
pub fn erste_zeile(text: &str) -> &str {
    text.lines().next().unwrap_or("")
}
// Implizit: fn erste_zeile<'a>(text: &'a str) -> &'a str
// Die Rückgabe lebt so lange wie der Input.

fn main() {
    let log = String::from("Erste Zeile\nZweite Zeile");
    let zeile = erste_zeile(&log);
    println!("{zeile}");
}

Der Compiler garantiert: zeile lebt nur so lange wie log. Kein Dangling möglich.

Strukt mit Lifetime für Zero-Copy-Parser

Rust Parser-Pattern
pub struct LogZeile<'a> {
    pub timestamp: &'a str,
    pub level: &'a str,
    pub message: &'a str,
}

pub fn parse_log(zeile: &str) -> Option<LogZeile<'_>> {
    let (timestamp, rest) = zeile.split_once(' ')?;
    let (level, message) = rest.split_once(' ')?;
    Some(LogZeile { timestamp, level, message })
}

fn main() {
    let log = String::from("2026-05-20T10:00:00 INFO Hallo Welt");
    if let Some(l) = parse_log(&log) {
        println!("{} {} {}", l.timestamp, l.level, l.message);
    }
    // l ist hier weg — log darf jetzt freigegeben werden
}

LogZeile<'a> hält drei &str-Felder, die in den Original-String zeigen. Kein einziger Klon — und der Compiler stellt sicher, dass LogZeile nicht länger lebt als log.

Closure-Capture mit Lifetime

Rust Closure
fn main() {
    let basis = String::from("PREFIX: ");
    let drucke = |suffix: &str| {
        println!("{basis}{suffix}");
    };
    drucke("Hi");
    drucke("Welt");
}

Die Closure capturet basis als shared Borrow. Sie darf nicht länger leben als basis — der Compiler stellt das sicher.

Globale Konstanten mit 'static

Rust Static Daten
const FEHLER_MELDUNGEN: &[(&str, &str)] = &[
    ("E001", "Ungültige Eingabe"),
    ("E002", "Fehlende Berechtigung"),
    ("E003", "Datenbank-Fehler"),
];

pub fn finde_meldung(code: &str) -> &'static str {
    FEHLER_MELDUNGEN.iter()
        .find(|(c, _)| *c == code)
        .map(|(_, m)| *m)
        .unwrap_or("Unbekannter Fehler")
}

fn main() {
    let m = finde_meldung("E001");
    println!("{m}");
}

Die Strings liegen im Binary — sie haben 'static-Lifetime. Die Funktion gibt eine &'static str zurück, die für die gesamte Programm-Laufzeit gültig ist.

Thread mit Move-Closure

Rust Thread-Capture
use std::thread;

fn main() {
    let daten = vec![1, 2, 3, 4, 5];
    // Closure captured daten by-move — thread::spawn verlangt 'static
    let handle = thread::spawn(move || {
        let summe: i32 = daten.iter().sum();
        println!("Summe: {summe}");
    });
    handle.join().unwrap();
}

thread::spawn verlangt, dass die Closure 'static lebt — sie darf keine Referenzen auf den Aufrufer-Scope haben, weil der Thread länger leben könnte. move || verschiebt die Daten in den Thread.

Iterator-Adapter mit Lifetime

Rust Iterator-Lifetime
pub fn finde_keys<'a>(text: &'a str, praefix: &'a str) -> impl Iterator<Item = &'a str> + 'a {
    text.lines()
        .filter(move |l| l.starts_with(praefix))
        .map(|l| l.trim_start_matches(|c: char| !c.is_whitespace()))
        .map(str::trim)
}

Der Iterator lebt so lange wie sowohl text als auch praefix. Compile-Zeit-Garantie, dass kein Element des Iterators auf freigegebenen Speicher zeigt.

Reference in Cache

Rust Cache mit owned Daten
use std::collections::HashMap;

pub struct Cache {
    eintraege: HashMap<String, String>,
}

impl Cache {
    pub fn lookup(&self, key: &str) -> Option<&str> {
        self.eintraege.get(key).map(|s| s.as_str())
    }
}
// Implizit: fn lookup<'a>(&'a self, key: &str) -> Option<&'a str>
// Die Rückgabe lebt so lange wie der Cache.

Der Cache besitzt seine Daten (Strings in HashMap). Die Lookup-Methode gibt eine Referenz zurück, die mit dem Cache-Lifetime gekoppelt ist.

Besonderheiten

Dangling References sind in Safe Rust unmöglich.

Das ist eine der Kern-Garantien der Sprache. In C/C++ ist es die Hauptquelle vieler Sicherheitslücken. Rust beweist zur Compile-Zeit, dass jede Referenz auf gültigen Speicher zeigt. Diese Garantie ist kostenlos zur Laufzeit.

Lifetimes sind ein Compile-Zeit-Konzept.

Sie haben keine Repräsentation im Binary — keine Laufzeit-Checks, keine Speicher-Kosten. Sie sind reine Annotationen für den Borrow Checker. Im Maschinencode existieren sie nicht.

Lifetime-Elision deckt 95 % der Fälle ab.

Du musst Lifetimes nur dann explizit schreiben, wenn der Compiler sie nicht herleiten kann — typischerweise bei Funktionen mit mehreren Eingabe-Referenzen und einer Rückgabe-Referenz. Sonst macht der Compiler es automatisch.

E0597 ist der häufigste Lifetime-Fehler.

„Borrow does not live long enough" tritt auf, wenn eine Referenz länger lebt als ihr Ziel. Der Compiler zeigt exakt: wo geborgt wurde, wo das Ziel gedroppt wurde, wo die Referenz später genutzt wird.

'static heißt nicht „im Binary“.

Es heißt „lebt mindestens so lange wie das Programm". String-Literale sind 'static, aber auch dynamisch über Box::leak erzeugte Werte können 'static sein. Threads, die potentiell länger laufen als ihr Spawner, verlangen 'static für ihre Captures.

Strukturen mit Referenz-Feldern brauchen Lifetimes.

struct S { r: &T } ist Compile-Fehler. Lösung: struct S<'a> { r: &'a T }. Eine Struct-Instanz ist dann an die Lebenszeit ihrer Referenz-Felder gebunden.

Temporary Lifetime Extension rettet einfache Fälle.

let r = &String::from("Hi"); funktioniert, weil Rust die Lebenszeit des Temporary verlängert. Bei komplexeren Patterns greift das nicht — dann lieber explizit let s = ...; let r = &s;.

Lifetime-Annotationen ändern nicht die Laufzeit, nur die Prüfung.

Eine Funktion mit <'a> macht zur Laufzeit nichts anderes als eine ohne. Lifetimes sind ein Vertrag mit dem Compiler — er prüft, dass der Vertrag eingehalten wird, fügt aber keine Checks ein.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu References & Borrowing

Zur Übersicht