Lifetimes sind das Konstrukt, das Rust einzigartig macht. Andere Sprachen lösen Speicher-Sicherheit über Garbage Collection (Go, Java, Python) oder lassen den Programmierer manuell Pointer verwalten und hoffen auf Disziplin (C, C++). Rust geht einen dritten Weg: der Borrow-Checker prüft zur Compile-Zeit, dass jede Referenz ein gültiges Objekt referenziert — keine dangling references, keine use-after-free, keine double-free. Die Sprache, in der das ausgedrückt wird, sind Lifetimes: jede Referenz hat eine Lebenszeit, und der Compiler verfolgt, dass diese mit der Lebenszeit des referenzierten Objekts kompatibel ist. Dieses Kapitel führt von den Grundbegriffen bis zu fortgeschrittenen Themen wie Higher-Ranked Trait Bounds und Variance.
Das Problem
In C/C++ kannst du folgendes schreiben:
const char* gib_zeiger() {
char buffer[] = "Hello";
return buffer; // ← gibt Pointer auf lokalen Stack-Speicher zurück
}
int main() {
const char* ptr = gib_zeiger();
printf("%s\n", ptr); // Undefined behavior: buffer ist weg
return 0;
}Der Pointer ptr referenziert Speicher, der nach Rückkehr aus gib_zeiger ungültig ist. Der Compiler warnt vielleicht (mit -Wall), aber das Programm kompiliert. Zur Laufzeit kann alles passieren: Crash, Müll-Daten, oder zufällig korrektes Verhalten — Undefined Behavior.
In Rust kompiliert die Entsprechung nicht:
fn gib_zeiger() -> &str {
let buffer = String::from("Hello");
&buffer // FEHLER: buffer wird beim Funktions-Ende dropped
}
// error[E0106]: missing lifetime specifier
// error[E0515]: cannot return reference to local variable `buffer`Der Compiler erkennt: die Referenz auf buffer würde länger leben als buffer selbst. Das ist eine dangling reference — verboten.
Dieser Schutz ist nicht eine zusätzliche Linter-Warnung, sondern eine harte Compile-Regel. Code, der dangling references erzeugen würde, wird abgelehnt. Das ist die Kern-Garantie: Rust-Programme haben keine Speicher-Sicherheits-Bugs in der Form von dangling references, use-after-free, double-free, oder data-races — alles zur Compile-Zeit verhindert.
Die Grundbegriffe
Scope
Jede Variable hat einen Scope — den Code-Bereich, in dem sie gültig ist. Klassisch endet der Scope am } ihres Blocks.
fn main() {
let x = 42; // Scope von x beginnt
{
let y = 10; // Scope von y beginnt
println!("{x} {y}");
} // Scope von y endet — y wird dropped
println!("{x}"); // Nur x noch gültig
} // Scope von x endetBorrow / Referenz
Eine Referenz (&T oder &mut T) borgt einen Wert. Sie ist ein Pointer mit Compiler-Garantien.
fn main() {
let s = String::from("hello");
let r: &String = &s; // r borrowt s
println!("{r}"); // Lesender Zugriff über die Referenz
println!("{s}"); // s ist weiterhin nutzbar
}Lifetime
Eine Lifetime ist der Code-Bereich, in dem eine Referenz gültig sein muss. Der Compiler vergibt jeder Referenz implizit eine Lifetime und prüft, dass sie nicht länger lebt als der referenzierte Wert.
fn main() {
let r;
{
let s = String::from("hello");
r = &s; // r borrowt s — Lifetime von r ⊆ Scope von s
} // s wird dropped
// println!("{r}"); // FEHLER: r würde s überleben
}Der Compiler lehnt das ab, weil die Lifetime von r (bis zum Ende von main) länger wäre als die Lifetime von s (bis zum Ende des inneren Blocks).
Drei Welten
Das Lifetime-System ist nicht eine einzige Mechanik, sondern drei zusammenwirkende:
-
Implizite Lifetimes — der Compiler verfolgt für jede Referenz automatisch eine Lebenszeit. In den allermeisten Fällen merkst du das nicht.
-
Lifetime-Elision — der Compiler nutzt Regeln, um Lifetimes in Funktions-Signaturen automatisch abzuleiten. Du musst nicht jedes
&strexplizit annotieren. -
Explizite Lifetime-Annotations — wenn der Compiler nicht selbst entscheiden kann, musst du Lifetimes mit
'a,'b, etc. annotieren. Klassisch in komplexen Funktions-Signaturen, Structs mit Referenzen, und Trait-Bounds.
// Implizite Lifetimes — nichts annotiert, Compiler macht alles
fn ersten_char(s: &str) -> Option<char> {
s.chars().next()
}
// Lifetime-Elision — Compiler leitet von der einzigen Input-Lifetime ab
fn ersten_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// Explizite Annotation — bei mehreren Inputs muss der Programmierer klären
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}Im Alltag sind die ersten beiden Welten unsichtbar. Du schreibst Code, der Referenzen nutzt, und der Compiler kümmert sich. Bei komplexeren Signaturen brauchst du gelegentlich explizite Annotations — und das ist genau das, was den Ruf von Rust als „Lifetime-Sprache" prägt.
Borrowing-Regeln
Lifetimes wirken zusammen mit den Borrowing-Regeln (siehe Ownership-Kapitel):
- Zu jedem Zeitpunkt: entweder eine mutable Referenz ODER beliebig viele immutable Referenzen — niemals beides gleichzeitig.
- Eine Referenz darf den referenzierten Wert nicht überleben.
Lifetimes sind das Werkzeug, mit dem Regel 2 formal ausgedrückt und vom Compiler geprüft wird. Regel 1 funktioniert ohne explizite Lifetime-Syntax, aber unter der Haube nutzt der Compiler die Lifetime-Information für die Konflikt-Erkennung.
fn main() {
let mut v = vec![1, 2, 3];
let r1 = &v[0]; // immutable Borrow
let r2 = &v[1]; // weitere immutable Borrows OK
println!("{r1} {r2}");
// ab hier sind r1, r2 nicht mehr genutzt → ihre Lifetime endet
let m = &mut v; // mutable Borrow — jetzt erlaubt
m.push(4);
}Der Compiler nutzt Non-Lexical Lifetimes (NLL): die Lifetime einer Referenz endet bei ihrem letzten Use, nicht erst am Block-Ende. Dadurch wird viel mehr Code akzeptiert, als die strenge Lese-der-Block-Endet-Regel zulassen würde.
Was Lifetimes NICHT tun
Eine häufige Verwirrung: Lifetimes sind keine Laufzeit-Mechanik. Sie sind reine Compile-Zeit-Konzepte.
- Lifetimes erzeugen keinen Code im Binary.
- Lifetimes haben keinen Speicher-Overhead zur Laufzeit.
- Lifetimes können nicht zur Laufzeit verändert werden.
- Lifetimes sind kein Garbage Collector.
Was Lifetimes tatsächlich tun: dem Compiler erlauben zu prüfen, ob deine Verwendung von Referenzen sicher ist. Wenn ja, kompiliert dein Code. Wenn nein, gibt es einen Compile-Fehler. Zur Laufzeit gibt es nur normalen Pointer-Code — wie in C, nur garantiert sicher.
Was dich in diesem Kapitel erwartet
Das Kapitel hat zwei thematische Bögen.
Bogen 1: Grundlagen
Die ersten Artikel führen schrittweise vom Konzept zur Syntax:
- Was sind Lifetimes — die Grundmechanik anhand einfacher Beispiele
- Lifetime-Annotations — die
'a-Syntax, wann und wie nutzen - Lifetimes in Funktionen — Input- und Output-Lifetimes, mehrere Parameter
- Lifetimes in Structs — Strukturen, die Referenzen halten
- Lifetime-Elision — die drei Regeln des Compilers für automatische Ableitung
- Static-Lifetime —
'staticund seine besondere Rolle
Bogen 2: Fortgeschritten
Die späteren Artikel decken die Themen ab, die meist erst nach den ersten Stolperfallen ins Blickfeld kommen:
- Lifetime-Bounds —
T: 'aals Constraint - Higher-Ranked Trait Bounds (HRTB) —
for<'a>-Syntax - Lifetimes in impl-Blöcken —
impl<'a>und Methoden mit Lifetimes - Subtyping und Variance — wann Lifetimes kompatibel sind, kovariant vs. invariant
Wie liest du am besten?
Wenn Lifetimes neu für dich sind: linear durcharbeiten. Die Konzepte bauen aufeinander auf, und ohne die Grundlagen sind die fortgeschrittenen Themen Magie.
Wenn du schon Erfahrung hast: nimm dir „Lifetime-Elision" und „Higher-Ranked Trait Bounds" — das sind die Bereiche, in denen sich oft Wissenslücken zeigen.
Mini-Tour
Hier eine kurze Tour durch die Konzepte, die das Kapitel im Detail entfaltet.
Eine einfache Funktion mit Lifetimes
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world!");
let l = longest(&s1, &s2);
println!("Längster: {l}");
}'a ist eine Lifetime-Variable. Die Signatur sagt: beide Inputs und der Output haben dieselbe Lifetime 'a. Praktisch heißt das: der zurückgegebene Wert lebt höchstens so lange wie der kürzer-lebende der beiden Inputs.
Struct mit Referenz
struct Auszug<'a> {
text: &'a str,
}
fn main() {
let dokument = String::from("Hello World");
let auszug = Auszug { text: &dokument[..5] };
println!("{}", auszug.text);
}Wenn ein Struct Referenzen hält, brauchst du eine Lifetime-Annotation. Auszug<'a> sagt: der Struct enthält eine Referenz, die mindestens 'a lebt — die Struct-Instanz darf den referenzierten Wert nicht überleben.
'static
// String-Literale haben 'static — sie leben so lange wie das Programm
let s: &'static str = "Hello";
// Konstanten haben 'static
const NAME: &'static str = "Rust";'static ist eine spezielle Lifetime — sie umfasst die gesamte Programm-Laufzeit. String-Literale, Konstanten, und über Box::leak allozierter Speicher haben 'static.
Lifetime-Bound
use std::fmt::Display;
fn drucke_in_thread<T: Display + Send + 'static>(value: T) {
std::thread::spawn(move || {
println!("{value}");
}).join().unwrap();
}T: 'static ist ein Bound — T darf keine geliehenen Daten enthalten, die nicht-static-Lifetime hätten. Klassisch in Thread- und Async-APIs.
Interessantes
Lifetimes garantieren Speicher-Sicherheit zur Compile-Zeit.
Dangling references, use-after-free, double-free — alle verhindert ohne Garbage Collector. Der Borrow-Checker nutzt Lifetime-Information, um problematische Code-Pfade abzulehnen.
Lifetimes sind reine Compile-Zeit-Konzepte.
Kein Speicher-Overhead, kein Runtime-Code. Zur Laufzeit verhalten sich Referenzen wie normale Pointer in C — nur garantiert gültig.
Drei Welten: implizit, elidiert, explizit.
Im Alltag sind 90% der Lifetimes implizit oder elidiert (vom Compiler abgeleitet). Nur in komplexen Signaturen (Structs mit Referenzen, Funktionen mit mehreren Input-Refs) musst du explizit annotieren.
Non-Lexical Lifetimes (NLL) machen das System pragmatisch.
Die Lifetime einer Referenz endet bei ihrem letzten Use, nicht erst am Block-Ende. Viel mehr Code wird akzeptiert, als die strenge alte Regel zulassen würde.
Lifetimes wirken zusammen mit Ownership/Borrowing.
Sie sind nicht isoliert, sondern Teil eines Gesamtsystems: Ownership (wer besitzt einen Wert), Borrowing (wer darf borgen), Lifetimes (wie lange muss etwas leben). Alle drei greifen ineinander.
'static ist die längstmögliche Lifetime.
Programm-Laufzeit lang. String-Literale, Konstanten, und Box::leak-Speicher haben 'static. Häufig als Bound für Thread-Closures und langlebige Daten gefordert.
Lifetime-Elision spart viel Annotation.
Drei Regeln (siehe Elision-Artikel) erlauben dem Compiler, in den häufigsten Fällen die Lifetimes selbst abzuleiten. Du schreibst fn foo(s: &str) -> &str ohne 'a, weil die Regeln greifen.
Lifetimes lernt man durch Tun.
Die Syntax ist auf den ersten Blick fremdartig, das Konzept aber klar: „Wie lange muss diese Referenz gültig sein?". Wer es einmal verstanden hat, sieht den Borrow-Checker als Hilfe, nicht als Hürde.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Validating References with Lifetimes
- Rust Reference – Lifetime Elision
- The Rustonomicon – Lifetimes
- Rust Blog – Non-Lexical Lifetimes