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}");
}

Die einfachste und sicherste Lösung gegen Dangling-Risiken: die Funktion produziert einen neuen, owned Wert und gibt ihn by-value zurück. String als Rückgabewert ist ein Move — der Wert wandert aus der Funktion in den Aufrufer, ohne dass es jemals eine Referenz auf einen lokalen Stack-Wert gibt.

Im Gegensatz dazu wäre &str als Rückgabewert ein Lifetime-Problem: woher käme die Referenz? Nicht aus einem Funktions-Parameter (die sind nur Borrows, sie überleben die Funktion nicht), nicht aus einer lokalen Variable (gleiches Problem). Die einzige Möglichkeit wäre 'static — also ein String-Literal oder ein leak-Ed Wert. Bei dynamisch erzeugten Inhalten ist String die richtige Wahl.

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}");
}

Die Funktion erste_zeile produziert keinen neuen String — sie gibt eine Referenz auf einen Teilbereich des Inputs zurück. Dank Lifetime-Elision schreibt der Compiler implizit fn erste_zeile<'a>(text: &'a str) -> &'a str — die Rückgabe-Referenz ist an die Lifetime des Inputs gebunden. zeile lebt nicht länger als log.

Diese Form ist sehr effizient — es entsteht keine Allokation. Der Preis ist die Lifetime-Bindung: solange zeile lebt, kann log nicht freigegeben oder verändert werden. Bei einem temporären Slice in einer kurzen Schleife ist das ideal; bei einer langlebigen Sub-String-Referenz mit kurzem Original-String kann es zu Problemen führen.

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
}

Zero-Copy-Parsing ist eine der wichtigsten Performance-Techniken bei textbasierten Formaten. Statt für jedes Feld eine neue String-Allokation zu machen, hält das Resultat-Struct nur Slice-Referenzen in den Original-Buffer. Bei einer Log-Datei mit Millionen Zeilen sparst du damit Millionen von Allocations.

Der Lifetime-Parameter 'a an LogZeile bindet die Lebenszeit aller drei Felder an dieselbe Quelle. LogZeile darf nicht länger leben als das, woraus seine Strings stammen. Im Beispiel ist das log, ein lokaler String mit Funktions-Scope — l lebt darin und ist sicher.

Diese Art von API ist gleichermaßen elegant und disziplinarisch: der Aufrufer muss die Lifetime-Beziehungen verstehen, aber dafür bekommt er extreme Performance ohne versteckte Kosten.

Closure-Capture mit Lifetime

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

Closures, die Werte aus dem umgebenden Scope verwenden, capturen sie standardmäßig per Borrow. Die Closure drucke hält einen &basis-Borrow, der so lange lebt wie die Closure selbst. Wenn basis vor der Closure freigegeben würde, hätten wir eine Dangling Reference — der Compiler verhindert das.

Sichtbar wird die Lifetime-Beziehung erst, wenn du die Closure aus dem Scope herausgeben willst — etwa in einem Box<dyn Fn(&str)> zurückgeben. Dann verlangt der Compiler eine explizite Lifetime, oder du musst move benutzen, um basis in die Closure zu moven.

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}");
}

Eine Lookup-Tabelle als const mit String-Literalen ist ein praktisches Pattern, das die Lifetime-Mechanik gut zeigt. Die Strings ("E001", "Ungültige Eingabe", etc.) liegen im Read-Only-Datensegment des Binaries und haben damit 'static-Lifetime — sie existieren so lange wie der Prozess läuft.

Die Funktion finde_meldung gibt eine &'static str zurück: entweder aus der Tabelle (alle Strings dort sind 'static) oder der Default "Unbekannter Fehler" (selbst ein Literal). Der Aufrufer kann den zurückgegebenen String beliebig lange behalten, ohne Lifetime-Sorgen. Das ist nur dank der 'static-Lifetime möglich — bei dynamischen Strings müsste die Rückgabe entweder String (owned) oder &'a str mit Input-Lifetime sein.

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();
}

Threads sind das paradigmatische Beispiel für 'static-Anforderungen. Wenn thread::spawn eine Closure entgegennimmt, hat die JoinHandle selbst keine Lifetime-Bindung — der Thread könnte theoretisch unbegrenzt lange laufen, vielleicht sogar nach Ende der main-Funktion. Wenn die Closure Borrows in den Aufrufer-Scope halten würde, könnten sie nach dem Drop des Aufrufer-Scope dangling werden.

move || löst das, indem alle Captures in die Closure gemoved werden. Damit besitzt die Closure ihre Daten selbst und ist 'static. Diese Disziplin ist nicht eine Schikane — sie ist genau das, was Memory-Safety in Multi-Threaded-Code möglich macht, ohne dass Locks und Synchronisation in jeden Zugriff eingebaut werden müssen.

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.

Ein Cache mit owned Daten ist eine sehr saubere Struktur. Die HashMap besitzt ihre Keys und Values als String — sie sind nicht durch externe Lifetimes gebunden. Die Lookup-Methode gibt eine &str zurück, die durch Lifetime-Elision implizit &'a self-Lifetime erhält: der zurückgegebene Slice lebt so lange wie der Cache selbst.

Damit ist die API solide gegen Use-after-Free. Der Aufrufer kann die &str so lange behalten, wie er den Cache hat. Wenn er den Cache freigibt, läuft Drop für die HashMap und damit für alle Strings — dann ist die &str aber auch garantiert nicht mehr in Verwendung, weil der Compiler die Lifetime-Bindung erzwingt.

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