Eine Lifetime-Annotation ist Syntax, mit der du dem Compiler explizit sagst, welche Lifetimes Referenzen in einer Signatur haben. Im Alltag merkst du davon wenig — die meisten Annotations werden vom Compiler über Elision-Regeln automatisch eingefügt. Aber sobald eine Signatur mehrere Referenzen hat oder ein Struct Referenzen speichert, brauchst du die Syntax. Dieser Artikel zeigt sie systematisch: wie Lifetime-Parameter deklariert, wie sie an Referenzen gebunden, und wie sie zueinander in Beziehung gesetzt werden.
Die Grundsyntax
Eine Lifetime-Annotation beginnt mit einem Apostroph (') gefolgt von einem Namen. Konvention: kleine Buchstaben, beginnend bei 'a.
// Variable mit explizitem Lifetime-annotiertem Typ
let s: &'static str = "Hello";
// Funktion mit Lifetime-Parameter
fn first<'a>(s: &'a str) -> &'a str {
&s[..1]
}Drei Bestandteile zu unterscheiden:
- Lifetime-Parameter-Deklaration:
<'a>in der Funktions-Signatur — führt einen neuen Lifetime-Namen ein - Lifetime-Annotation an einer Referenz:
&'a str— die Referenz hat Lifetime'a - Lifetime-Name:
'a— ein Identifier, der eine Lifetime benennt
'a selbst hat keine spezielle Bedeutung — es ist nur ein Name. 'foo wäre auch syntaktisch gültig. Konvention ist 'a, 'b, 'c für anonyme Lifetimes und sprechende Namen ('src, 'dst) bei komplexen Signaturen.
Lifetime-Parameter in Funktionen
Funktions-Signaturen führen Lifetime-Parameter in spitzen Klammern ein, genau wie Type-Parameter.
// Eine Lifetime 'a, geteilt zwischen Input und Output
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}Die Signatur sagt:
- Beide Inputs (
aundb) und der Output haben dieselbe Lifetime'a - Der Compiler wählt
'aso, dass alle drei kompatibel sind — typischerweise als die kürzere der Input-Lifetimes - Der zurückgegebene Wert darf nicht länger leben als der kürzer-lebende Input
# 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 r;
{
let s2 = String::from("longer string");
r = longest(&s1, &s2);
println!("{r}"); // OK: r noch im Scope von s2
} // s2 wird dropped
// println!("{r}"); // FEHLER: r könnte auf s2 zeigen
}Die Lifetime 'a wird so gewählt, dass sie nicht länger ist als die kürzere Input-Lifetime — hier s2. Nach dem inneren Block ist r nicht mehr nutzbar.
Mehrere Lifetime-Parameter
Bei mehreren Inputs mit unterschiedlichen Lifetimes deklarierst du mehrere Lifetime-Parameter.
// Output an die erste Lifetime gebunden — zweite Lifetime ist frei
fn first_of<'a, 'b>(primary: &'a str, secondary: &'b str) -> &'a str {
println!("Secondary war: {secondary}");
primary
}
fn main() {
let s1 = String::from("hello");
let r;
{
let s2 = String::from("temporary");
r = first_of(&s1, &s2); // r zeigt auf s1, nicht auf s2
} // s2 dropped — kein Problem für r
println!("{r}"); // OK: r zeigt auf s1
}Hier ist 'a (Lifetime von primary und Output) unabhängig von 'b (Lifetime von secondary). Der Output ist nur an 'a gebunden, also nur an die Lebenszeit von s1. s2 darf vorher dropped werden.
Wenn dagegen beide Inputs als 'a deklariert wären, müsste der Compiler eine gemeinsame Lifetime finden und der Output wäre an die kürzere gebunden.
Lifetime-Relationen
Lifetime-Parameter können Beziehungen haben: 'a: 'b bedeutet „'a lebt mindestens so lange wie 'b".
// 'a muss mindestens so lange leben wie 'b
fn outlive<'a: 'b, 'b>(long: &'a str, short: &'b str) -> &'b str {
if long.len() > short.len() { long } else { short }
}Die Constraint 'a: 'b (lies: „a outlives b") sagt: jeder Code-Punkt in 'b liegt auch in 'a. Damit darf eine Referenz mit Lifetime 'a an einer Stelle verwendet werden, wo eine Referenz mit Lifetime 'b erwartet wird.
Diese Outlives-Constraints sind selten in Funktions-Signaturen explizit, aber wichtig in komplexeren Generic-Code und bei Lifetime-Bounds an Type-Parametern.
Lifetime an mut-Referenzen
Mutable Referenzen haben auch Lifetimes. Syntax: &'a mut T.
fn modify<'a>(s: &'a mut String) -> &'a String {
s.push_str(" world");
s
}
fn main() {
let mut s = String::from("Hello");
let r = modify(&mut s);
println!("{r}"); // "Hello world"
}Beachte die Reihenfolge: &'a mut T — Lifetime kommt zwischen & und mut. Funktional analog zu &'a T: die mut-Referenz darf nicht länger leben als der referenzierte Wert.
Die spezielle Lifetime '_
'_ ist die anonyme oder placeholder Lifetime. Du nutzt sie, wenn du eine Lifetime explizit erwähnen willst, aber nicht benennen musst — der Compiler soll sie selbst wählen.
struct Buffer<'a> { data: &'a [u8] }
impl Buffer<'_> {
// '_ bedeutet: gleiche Lifetime wie der Buffer
pub fn len(&self) -> usize {
self.data.len()
}
}Statt jedes Mal impl<'a> Buffer<'a> zu schreiben und 'a selbst nicht zu nutzen, schreibst du impl Buffer<'_>. Cleaner.
Auch in Funktions-Signaturen mit Lifetime-elision-Verstärkung: fn parse(input: &str) -> Result<MyType<'_>, Error> zeigt explizit, dass MyType eine Lifetime hat, ohne sie zu benennen.
Praxis: häufige Annotation-Pattern
Funktion mit Output abhängig von einem Input
// Output-Lifetime gebunden an den ersten Input
fn header<'a>(data: &'a [u8], _trailer: &[u8]) -> &'a [u8] {
&data[..4.min(data.len())]
}
fn main() {
let main_data = vec![0; 100];
let result;
{
let trailer = vec![1, 2, 3];
result = header(&main_data, &trailer);
// result hängt nur an main_data, nicht an trailer
}
println!("Header-Length: {}", result.len());
}Klassisches Pattern: ein Input liefert das Output, der andere ist nur Konfigurations-Parameter mit anderer Lifetime.
Funktion mit Output abhängig von beiden Inputs
// Output an beide Inputs gebunden (gleiche Lifetime)
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}Wenn der Output je nach Logik aus einem von mehreren Inputs stammen kann, muss eine gemeinsame Lifetime gewählt werden.
Struct mit Referenz-Feld
pub struct Parser<'src> {
input: &'src str,
position: usize,
}
impl<'src> Parser<'src> {
pub fn neu(input: &'src str) -> Self {
Parser { input, position: 0 }
}
pub fn rest(&self) -> &'src str {
&self.input[self.position..]
}
}Structs, die Referenzen halten, brauchen einen Lifetime-Parameter. Konvention: sprechende Namen wie 'src (für Source), 'doc (für Document) machen die Bedeutung klar.
Methode mit zusätzlicher Lifetime
struct Container<'a> { items: Vec<&'a str> }
impl<'a> Container<'a> {
// Methode hat eigene Lifetime 'b für temporäre Operation
fn search<'b>(&self, needle: &'b str) -> Option<&'a str> {
self.items.iter().find(|s| s.contains(needle)).copied()
}
}Die Methode search hat eine eigene Lifetime 'b für das needle-Argument. Der Output ist an 'a gebunden (die Lifetime des Containers).
Multiple-Output via Tuple
fn split_first<'a>(s: &'a str) -> (&'a str, &'a str) {
let mid = s.char_indices().nth(1).map(|(i, _)| i).unwrap_or(s.len());
s.split_at(mid)
}
fn main() {
let text = "Hello";
let (first, rest) = split_first(text);
println!("first={first}, rest={rest}");
}Mehrere Output-Referenzen, alle an die gleiche Input-Lifetime gebunden.
Trait-Objekt mit Lifetime
use std::fmt::Display;
fn first<'a>(items: &'a [Box<dyn Display + 'a>]) -> &'a (dyn Display + 'a) {
items.first().unwrap().as_ref()
}Trait-Objekte (dyn Trait) haben eine eigene Lifetime — dyn Display + 'a heißt: das Trait-Objekt lebt mindestens 'a. Klassisch nötig, wenn das Trait-Objekt geliehene Daten enthält.
Outlives in der Praxis
struct LongerContainer<'long, 'short: 'long> {
short_ref: &'short str,
long_ref: &'long str,
}
impl<'long, 'short: 'long> LongerContainer<'long, 'short> {
fn neu(short_ref: &'short str, long_ref: &'long str) -> Self {
Self { short_ref, long_ref }
}
}'short: 'long sagt: 'short lebt mindestens so lange wie 'long. Damit kann eine &'short str an einer Stelle stehen, wo &'long str erwartet wird.
Interessantes
Lifetime-Annotations beginnen mit Apostroph.
'a, 'b, 'static. Konvention: kleine Buchstaben, beginnend bei 'a. Bei komplexen Signaturen sprechende Namen wie 'src oder 'doc.
Drei Bestandteile: Deklaration, Annotation, Name.
<'a> deklariert, &'a T annotiert eine Referenz, 'a ist der Name. Type-Parameter und Lifetime-Parameter teilen sich die <...>-Klammern.
Mehrere Lifetimes für unabhängige Inputs.
fn foo<'a, 'b>(a: &'a str, b: &'b str) — Inputs haben unabhängige Lebenszeiten. Output-Lifetime wählst du gezielt, je nachdem an welchen Input er gebunden ist.
Lifetime an gleiche Variable bindet beide an kürzere.
fn foo<'a>(a: &'a str, b: &'a str) -> &'a str — die kürzer-lebende Input-Lifetime bestimmt 'a und damit auch die Output-Lifetime.
Outlives-Syntax: 'a: 'b.
Heißt „a outlives b" — 'a lebt mindestens so lange wie 'b. Damit darf eine Ref mit 'a an Stellen mit 'b stehen. Selten explizit, aber wichtig für komplexe Generic-Code.
Mutable Refs: &'a mut T.
Reihenfolge: Apostroph kommt zwischen & und mut. Lifetime-Mechanik identisch zu immutable Refs.
'_ ist die anonyme Lifetime.
Sagt: „Compiler, wähl du eine passende Lifetime." Hilft bei impl Buffer<'_> statt impl<'a> Buffer<'a>, wenn du 'a selbst nicht im Body brauchst.
'static ist eine eingebaute Lifetime.
Bedeutet „lebt so lange wie das Programm". String-Literale, Konstanten, statisch alloziertes Memory. Mehr im Static-Lifetime-Artikel.