Die Übersicht hat den Rahmen abgesteckt: Lifetimes sind Compile-Zeit-Information für den Borrow-Checker. Dieser Artikel macht das konkret. Was tut der Borrow-Checker wirklich? Was bedeutet eine Lifetime mathematisch? Wie kannst du dir das Konzept als mentales Modell vorstellen, damit Lifetime-Fehler nicht wie Compiler-Magie wirken, sondern wie das vorhersehbare Resultat klarer Regeln? Wer das versteht, kann Lifetime-Code lesen und schreiben, ohne jedes Mal raten zu müssen.
Eine Lifetime ist eine Region
Mathematisch ist eine Lifetime eine Code-Region — eine Menge von Programm-Punkten, an denen eine Referenz gültig sein muss. Diese Region beginnt typischerweise dort, wo die Referenz erzeugt wird, und endet bei ihrem letzten Use.
fn main() {
let s = String::from("hello"); // ← s entsteht (Lifetime 'a beginnt)
let r = &s; // ┐ r entsteht (Lifetime 'b beginnt)
println!("{r}"); // │ ← letzter Use von r
// ┘ Lifetime 'b endet
println!("{s}"); // s noch genutzt
} // ← s endet, Lifetime 'a endetDie Lifetime von s ist die Region von Zeile 2 bis zum Ende von main. Die Lifetime von r ist die kleinere Region von Zeile 3 bis Zeile 4 (letzter Use). Der Borrow-Checker prüft: ist die Lifetime von r enthalten in der Lifetime von s? Ja — also gültig.
Das ist der zentrale Mechanismus: für jede Referenz gibt es eine Region, in der sie genutzt wird, und der Compiler prüft, dass diese Region innerhalb der Lebenszeit des Werts liegt, auf den die Referenz zeigt.
Was der Compiler intern macht
Wenn der Compiler ein Stück Code analysiert, gibt er jeder Referenz und jedem Wert eine interne Lifetime-Variable. Dann sammelt er Constraints aus dem Code und löst diese auf:
fn main() {
let s = String::from("hello"); // Wert mit Lifetime 'val_s
let r: &String = &s; // Referenz mit Lifetime 'ref_r
// Compiler erzeugt Constraint:
// 'ref_r ⊆ 'val_s
// d.h. die Referenz darf nicht länger leben als der Wert
println!("{r}");
}Der Compiler löst alle solche Constraints. Wenn sie alle erfüllbar sind, kompiliert dein Code. Wenn nicht — also wenn eine Referenz länger leben müsste als der Wert, auf den sie zeigt — gibt es einen Compile-Fehler.
In den meisten Fällen sind die Constraints trivial und du merkst nichts. Bei komplexeren Situationen (mehrere Referenzen, Lifetime in Structs, Trait-Objekte) wird die Constraint-Lösung anspruchsvoller, und der Compiler braucht Hilfe in Form von expliziten Annotationen.
Dangling References — was Lifetimes verhindern
Der Klassiker, den Lifetimes verhindern:
fn main() {
let r;
{
let x = 42;
r = &x; // Lifetime von r müsste länger sein als x
} // x wird hier dropped
// println!("{r}"); // Wäre dangling — verboten
}
// error[E0597]: `x` does not live long enough
// --> src/main.rs:5:17
// |
// 5 | r = &x;
// | ^^ borrowed value does not live long enough
// 6 | }
// | - `x` dropped here while still borrowedDer Compiler liest:
rlebt bis zum Ende von mainxlebt bis zum Ende des inneren Blocksr = &xwürde die Lifetime von&x(≤ Lifetime vonx) auf die Lifetime vonr(Ende von main) ausdehnen — Widerspruch!
Der Fehler wird abgelehnt. Genau das ist der Punkt: dieser Code würde in C einen klassischen use-after-free erzeugen. In Rust kompiliert er nicht.
Non-Lexical Lifetimes (NLL)
In alten Rust-Versionen (vor 2018) endete eine Lifetime immer am Block-Ende der Variable. Das war oft zu strikt.
fn main() {
let mut v = vec![1, 2, 3];
let r = &v[0]; // immutable Borrow
println!("{r}"); // letzter Use von r
v.push(4); // mutable Borrow — in altem Rust verboten
// (Konflikt: r noch im Scope, aber tatsächlich nicht mehr genutzt)
}Mit Non-Lexical Lifetimes (NLL, seit Rust 2018) endet die Lifetime von r bei ihrem letzten Use (nach println!), nicht erst am Block-Ende. Damit ist v.push(4) erlaubt — kein Konflikt mehr.
NLL macht den Borrow-Checker viel pragmatischer. Viele Konstellationen, die früher umgeschrieben werden mussten, kompilieren heute direkt. Wer noch alte Tutorials liest, trifft gelegentlich auf umständliche Workarounds, die heute nicht mehr nötig sind.
Lifetime ist eine TYPISCHE Compile-Zeit-Eigenschaft
Eine wichtige Einsicht: Lifetime ist Teil des Typs einer Referenz, nicht ein Laufzeit-Wert.
Wenn du &str schreibst, ist das eigentlich kurz für &'lt str mit einer kontextbestimmten Lifetime 'lt. Der Compiler trägt diese Lifetime im Typ-System mit und prüft Kompatibilität.
fn use_str<'a>(s: &'a str) {
// hier ist s: &'a str — mit Lifetime 'a im Typ
println!("{s}");
}
fn main() {
let s = String::from("hello");
use_str(&s);
// beim Aufruf wird 'a so gewählt, dass die Lifetime von &s mindestens 'a ist
}Anders gesagt: zwei Referenzen sind nur dann typgleich, wenn sie nicht nur denselben Wert-Typ haben, sondern auch dieselbe Lifetime. &'a str und &'b str sind unterschiedliche Typen, wenn 'a und 'b verschieden sind.
In der Praxis ist das relevant, wenn du Lifetimes in Funktions-Signaturen oder Struct-Definitionen annotieren musst — du gibst dem Compiler Typ-Information, die er sonst nicht ableiten könnte.
Mentales Modell für Lifetime-Code
Beim Lesen und Schreiben von Lifetime-Code hilft folgendes mentales Modell:
-
Jede Referenz hat eine Region, in der sie gültig sein muss. Diese Region beginnt bei der Erzeugung und endet beim letzten Use.
-
Jeder Wert hat eine Region, in der er existiert. Diese Region beginnt bei der Erzeugung und endet beim Drop (typischerweise am Block-Ende).
-
Constraints: für jede Referenz muss gelten — ihre Region ist enthalten in der Region des referenzierten Werts.
-
Compiler löst alle Constraints. Erfolg: Code kompiliert. Misserfolg: Compile-Fehler mit Details, welche Constraint verletzt wird.
-
Annotations machen Constraints explizit, wenn der Compiler sie nicht selbst ableiten kann (typisch: mehrere Input-Referenzen in einer Funktion, Lifetimes in Struct-Definitionen).
Mit diesem Modell sind Lifetime-Fehler nicht mysteriös: der Compiler sagt, welche Constraint verletzt ist, und du suchst nach der Stelle, wo eine Referenz länger genutzt wird als der referenzierte Wert.
Praxis: typische Lifetime-Situationen
Referenz aus Funktion zurückgeben
// Funktion gibt Referenz auf ihren Input zurück
fn first_char(s: &str) -> &str {
&s[..s.chars().next().map_or(0, |c| c.len_utf8())]
}
fn main() {
let text = String::from("Hello");
let c = first_char(&text);
println!("Erstes Zeichen: {c}");
}Der Output ist eine Referenz in den Input — der Compiler leitet das via Elision-Regeln ab. Die Lifetime des Outputs ist gebunden an die Lifetime des Inputs.
Referenz in lokale Variable speichern
fn main() {
let s = String::from("Hello");
let r = &s;
let r2 = r; // r2 = &s, derselbe Lifetime
println!("{r}, {r2}");
}Zuweisung einer Referenz an eine andere Variable kopiert nur den Pointer. Lifetimes bleiben gebunden an den ursprünglichen Wert.
Referenz mit kürzerer Lifetime
fn main() {
let s1 = String::from("longer-living");
{
let s2 = String::from("shorter-living");
let r: &str;
if s1.len() > s2.len() {
r = &s1;
} else {
r = &s2;
}
println!("{r}");
}
// r darf hier nicht mehr existieren — s2 ist dropped
}r kann auf s1 oder s2 zeigen — der Compiler nimmt die kürzere Lifetime (s2) und begrenzt r entsprechend. Nach dem inneren Block ist r nicht mehr nutzbar.
Mehrere Referenzen ohne Konflikt
fn main() {
let v = vec![1, 2, 3, 4, 5];
let a = &v[0];
let b = &v[2];
let c = &v[4];
println!("{a}, {b}, {c}");
}Mehrere immutable Referenzen auf dasselbe Vec sind erlaubt. Alle leben innerhalb der Lifetime von v.
Konflikt: Mutable während Immutable existiert
fn main() {
let mut v = vec![1, 2, 3];
let r = &v[0]; // immutable Borrow
// v.push(4); // FEHLER: mutable Borrow während immutable lebt
println!("{r}"); // letzter Use von r
v.push(4); // OK: r ist nicht mehr genutzt (NLL)
}Klassischer Borrow-Konflikt mit Lifetime-Bezug. Der Borrow-Checker erkennt: r ist eine immutable Referenz auf v, also darf v während r aktiv ist nicht mutiert werden.
Returning Reference to Local — verboten
// VERBOTEN: lokale Variable wird dropped, Referenz hängt
// fn build_string() -> &str {
// let s = String::from("hello");
// &s
// }
// error[E0515]: cannot return reference to local variable `s`
// Korrekt: owned String zurückgeben
fn build_string() -> String {
String::from("hello")
}Ein Klassiker. Die lokale Variable würde dropped, die Referenz wäre dangling. Lösung: Owned-Wert zurückgeben.
Static-Lifetime aus Literal
fn get_greeting() -> &'static str {
"Hello" // String-Literal hat 'static-Lifetime
}
fn main() {
let g = get_greeting();
println!("{g}");
}String-Literale sind in der Programm-Binary fest hinterlegt. Sie leben so lange wie das Programm — 'static. Diese Referenz darf aus einer Funktion zurückgegeben werden.
Interessantes
Lifetime = Code-Region, in der eine Referenz gültig sein muss.
Mathematisch eine Menge von Programm-Punkten. Beginnt bei der Erzeugung der Referenz, endet beim letzten Use (mit NLL).
Borrow-Checker prüft: Ref-Region ⊆ Wert-Region.
Jede Referenz darf nicht länger leben als der Wert, auf den sie zeigt. Wenn der Compiler diese Constraint nicht erfüllen kann, gibt es einen Fehler.
Lifetime ist Teil des Typs einer Referenz.
&'a str und &'b str sind verschiedene Typen, wenn 'a und 'b verschieden sind. Compiler trägt Lifetime-Info im Typ-System mit.
Non-Lexical Lifetimes (NLL) machen das System pragmatisch.
Lifetime endet beim letzten Use, nicht am Block-Ende. Viel mehr Code wird akzeptiert, als es früher (vor 2018) der Fall war.
Lifetime-Fehler folgen klaren Regeln.
Wenn der Compiler ablehnt, sagt er welche Constraint verletzt ist und an welcher Stelle. Mit dem mentalen Modell „Region ⊆ Region" kannst du fast jeden Fehler nachvollziehen.
Lifetimes sind reine Compile-Zeit-Info.
Kein Code im Binary, kein Runtime-Overhead. Zur Laufzeit wirkt eine Rust-Referenz wie ein C-Pointer — nur dass der Compiler ihre Korrektheit bereits bewiesen hat.
Klassische Verbote: Ref auf lokale Variable zurückgeben.
Lokale Variable wird beim Funktions-Ende dropped, Referenz wäre dangling. Lösung: Owned-Wert zurückgeben oder Lifetime an Input-Ref binden.
Mit NLL ist das System lesbarer geworden.
Viele alte Tutorials zeigen umständliche Workarounds für inzwischen erlaubte Code-Pattern. Wenn du auf veraltete „Lifetime-Wahnsinn"-Beispiele triffst, lohnt sich ein zweiter Blick mit aktuellem Rust.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Lifetime Annotations in Function Signatures
- Rust Blog – Non-Lexical Lifetimes Announcement
- The Rustonomicon – Lifetimes Concept