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:
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:
fn erstelle_zahl() -> &i32 {
let x = 42;
&x // Fehler — x lebt nicht lang genug
}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 fromSchon 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:
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:
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 hereDer 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
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.
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.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
// 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
// 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
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:
// 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
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
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
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
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
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
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
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
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
- The Rust Book – Lifetimes
- Rust Reference – Lifetimes
- The Rustonomicon – Lifetimes
- rustc Error E0597
- rustc Error E0106