Wenn jede Funktions-Signatur mit Referenzen explizite 'a-Syntax bräuchte, wäre Rust unerträglich verbose. Der Compiler nutzt drei Elision-Regeln, um in den häufigsten Fällen Lifetimes automatisch abzuleiten. Diese Regeln sind nicht magisch — sie sind exakt spezifiziert, deterministisch und für den Programmierer nachvollziehbar. Wer sie kennt, weiß sofort, wann eine Signatur ohne Annotation auskommt und wann nicht.
Die drei Regeln
Der Compiler wendet die folgenden Regeln der Reihe nach auf jede Funktions-Signatur an, bevor er Lifetimes prüft. Die Regeln sind in der Rust-Reference fixiert:
Regel 1: Jeder elidierter Input-Lifetime-Position bekommt eine eigene Lifetime.
Regel 2: Wenn es genau eine Input-Lifetime gibt (nach Anwendung von Regel 1), wird diese allen Output-Lifetimes zugewiesen.
Regel 3: Wenn es mehrere Input-Lifetimes gibt, aber eine davon ist &self oder &mut self, wird die self-Lifetime allen Output-Lifetimes zugewiesen.
Wenn nach Anwendung dieser drei Regeln noch Output-Lifetimes ohne Wert sind, gibt es einen Compile-Fehler — du musst dann explizit annotieren.
Regel 1 im Detail
Jede Input-Referenz, die keine explizite Lifetime hat, bekommt eine neue eigene Lifetime.
// Was du schreibst:
fn process(a: &str, b: &str) {
println!("{a} {b}");
}
// Was der Compiler intern sieht (nach Regel 1):
fn process<'a, 'b>(a: &'a str, b: &'b str) {
println!("{a} {b}");
}Jede Input-Ref bekommt ihre eigene anonyme Lifetime. Bei zwei Inputs gibt es 'a und 'b. Bei drei: 'a, 'b, 'c. Etc.
Wenn die Funktion nichts zurückgibt, ist das vollständig — keine weiteren Regeln nötig.
Regel 2 im Detail
Wenn es genau eine Input-Lifetime gibt, wird sie allen Output-Lifetimes zugewiesen.
// Was du schreibst:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// Schritt 1 (Regel 1): jeder Input bekommt eigene Lifetime
// fn first_word<'a>(s: &'a str) -> &str
// Schritt 2 (Regel 2): genau eine Input-Lifetime → an Output
// fn first_word<'a>(s: &'a str) -> &'a strBei einer einzelnen Input-Ref greift die Regel: der Output erbt die Input-Lifetime. Sehr häufiger Fall, deckt die meisten Slice-Funktionen ab.
// Was du schreibst:
// fn pick(a: &str, b: &str) -> &str {
// if a.len() > b.len() { a } else { b }
// }
//
// Schritt 1 (Regel 1): 'a für a, 'b für b
// Schritt 2 (Regel 2): greift NICHT — zwei Input-Lifetimes
// Schritt 3 (Regel 3): greift NICHT — kein &self
//
// Compile-Fehler: missing lifetime specifierBei mehreren Input-Refs greift Regel 2 nicht. Der Compiler weiß nicht, an welche Input-Lifetime der Output gebunden sein soll. Du musst explizit annotieren.
Regel 3 im Detail
Wenn es mehrere Inputs gibt, aber einer davon &self oder &mut self ist, wird die self-Lifetime allen Outputs zugewiesen.
struct Cache<'a> { data: &'a str }
impl<'a> Cache<'a> {
// Was du schreibst:
fn lookup(&self, key: &str) -> &str {
if self.data.contains(key) { self.data } else { "" }
}
// Was der Compiler ableitet (Regel 1 → Regel 3):
// Regel 1: 'self_lt für &self, 'key_lt für key
// Regel 3: self-Lifetime an Output → 'self_lt
// fn lookup<'key_lt>(&self, key: &'key_lt str) -> &str
// (das Output hat self-Lifetime — automatisch)
}Bei Methoden ist Regel 3 sehr nützlich. Der Output wird typischerweise aus self extrahiert (z.B. ein Feld), nicht aus den Parametern. Die Regel modelliert diesen häufigen Fall.
Wann Elision scheitert
Die Regeln decken die häufigsten Fälle ab, aber nicht alle. Wenn nach Anwendung aller drei Regeln noch eine Output-Lifetime ungeklärt ist, gibt es einen Compile-Fehler.
// Fall 1: Mehrere Refs, kein &self
// fn pick(a: &str, b: &str) -> &str ← FEHLER
// Korrektur: explizit annotieren
fn pick<'a>(a: &'a str, b: &'a str) -> &'a str { a }
// Fall 2: Output-Ref ohne klare Quelle
// fn create() -> &str ← FEHLER (woher kommt die Ref?)
// Korrektur: 'static oder Owned
fn create() -> &'static str { "hello" }
// Fall 3: Komplexe Struct-Output
struct View<'a>(&'a str);
// fn build(a: &str, b: &str) -> View ← FEHLER (welche Lifetime hat View?)
// Korrektur: explizit
fn build<'a>(a: &'a str, _b: &str) -> View<'a> { View(a) }Wenn die Compiler-Meldung „missing lifetime specifier" kommt, ist eine der Elision-Regeln gescheitert. Lösung: manuelle Annotation.
Elision ändert nicht das Verhalten
Eine wichtige Klarstellung: Elision ist nur eine Schreibhilfe. Sie ändert nichts am Verhalten. Eine Funktion mit elidierten Lifetimes ist semantisch identisch zur expliziten Version.
// Elidiert:
fn first(s: &str) -> &str {
&s[..1.min(s.len())]
}
// Explizit (semantisch identisch):
fn first_explicit<'a>(s: &'a str) -> &'a str {
&s[..1.min(s.len())]
}Beide Funktionen sind identisch in Verhalten und Constraints. Die explizite Version ist nur länger zu schreiben.
Daraus folgt: wenn du dir nicht sicher bist, was Elision ableitet, schreib es explizit aus und prüf, ob es kompiliert. Das ist ein gutes Lernwerkzeug.
Praxis: Elision in der Wildbahn
Stdlib-Beispiele
// str::trim — Regel 2: ein Input, ein Output
// fn trim(&self) -> &str
// Elidiert: fn trim<'a>(&'a self) -> &'a str
// str::split_at — Regel 2 auf Tuple-Output
// fn split_at(&self, mid: usize) -> (&str, &str)
// Elidiert: fn split_at<'a>(&'a self, mid: usize) -> (&'a str, &'a str)
// Vec::iter — Regel 3 (self-basiert)
// fn iter(&self) -> Iter<'_, T>
// Elidiert: fn iter<'a>(&'a self) -> Iter<'a, T>Die meisten Stdlib-Signaturen sind elidiert. Sie zu lesen heißt, mental die Elision rückgängig zu machen.
Custom-Funktion mit klarer Elision
fn extract_first(s: &str) -> &str {
s.split('.').next().unwrap_or("")
}
// Regel 2: einziger Input, sein Lifetime an Output
// = fn extract_first<'a>(s: &'a str) -> &'a strMethode mit Regel 3
struct Logger { prefix: String }
impl Logger {
// Regel 1: 'a für &self, 'b für msg
// Regel 3: self-Lifetime an Output
// = fn format(&self, msg: &str) -> String + temporäres &str-Slice
// (aber Output ist String, also Owned — keine Elision-Frage)
fn format(&self, msg: &str) -> String {
format!("[{}] {msg}", self.prefix)
}
}Hier ist der Output ein owned String, daher keine Elision nötig.
Wenn Elision nicht reicht
// Output kann aus a ODER b kommen — manuell annotieren
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
// Output kommt nur aus a, b ist Konfig
fn extract_with_config<'a>(a: &'a str, _config: &str) -> &'a str {
&a[..3.min(a.len())]
}
// Output ist Struct mit Referenz — manuell
struct View<'a>(&'a str);
fn make_view<'a>(s: &'a str) -> View<'a> {
View(s)
}Drei häufige Fälle, in denen Elision nicht reicht und manuelle Annotation Pflicht ist.
Hybride: Elision plus explizite Annotation
// Mix: zwei Inputs, aber Output explizit an einen gebunden
// → der zweite Input bekommt elidierte Lifetime
fn select<'a>(primary: &'a str, _logging: &str) -> &'a str {
primary
}Du kannst manuell annotieren, was du explizit klären willst, und den Rest elidieren.
Methoden mit Multi-Input
struct Container<'a> { items: Vec<&'a str> }
impl<'a> Container<'a> {
// Methode mit zusätzlichem Input. Regel 3: self-Lifetime an Output
fn find(&self, needle: &str) -> Option<&str> {
self.items.iter().find(|s| s.contains(needle)).copied()
}
// Elidiert zu: fn find<'short>(&self, needle: &'short str) -> Option<&'a str>
}Bei Methoden mit zusätzlichen Refs greift Regel 3 — Output erbt self-Lifetime. Der Parameter needle hat eine eigene, kurze Lifetime, die nicht an den Output gebunden ist.
Closure-Argumente
// Funktion akzeptiert eine Closure, die Refs verarbeitet
fn apply<F>(s: &str, f: F) -> String
where F: Fn(&str) -> String
{
f(s)
}
fn main() {
let result = apply("hello", |x| x.to_uppercase());
println!("{result}");
}Closures haben oft elidierte Lifetimes. Bei komplexeren Closures braucht es manchmal Higher-Ranked Trait Bounds (siehe HRTB-Artikel).
Interessantes
Drei Elision-Regeln in fester Reihenfolge.
Regel 1: jeder Input bekommt eigene Lifetime. Regel 2: bei einer Input-Lifetime → an Output. Regel 3: bei &self → self-Lifetime an Output.
Regel 2 deckt den häufigsten Fall ab.
Single-Input + Single-Output ohne explizite Annotation: Compiler nimmt automatisch dieselbe Lifetime für beides. Klassischer Slice-Cut, Tokenizer, etc.
Regel 3 ist methoden-spezifisch.
Output von Methoden wird typischerweise aus self extrahiert. Die Regel modelliert das — Output erbt die self-Lifetime, andere Parameter sind unabhängig.
Elision ändert nichts am Verhalten.
Nur Schreibhilfe. Eine elidierte und eine explizite Signatur sind semantisch identisch. Wenn unklar, einfach explizit ausschreiben und prüfen.
Wenn Elision scheitert: missing lifetime specifier.
Klassische Compile-Meldung. Greift bei: mehreren Input-Refs ohne self, Output-Refs ohne klare Quelle, Output-Structs mit Lifetime-Parametern.
Bei Unsicherheit: mental Regeln durchspielen.
Schritt 1 (Regel 1): jedem Input eine Lifetime geben. Schritt 2 (Regel 2): wenn nur eine, an Output. Schritt 3 (Regel 3): wenn &self da, self an Output. Wenn am Ende Output ohne Lifetime: explizit annotieren.
Elision gilt nur für Funktions- und Methoden-Signaturen.
Nicht für Struct- oder Enum-Definitionen. Dort musst du Lifetime-Parameter immer explizit deklarieren.
Tuple- und Struct-Outputs erben Elision genauso.
fn split(s: &str) -> (&str, &str) — beide Output-Refs bekommen die Input-Lifetime. Alle Output-Positionen werden behandelt.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Lifetime Elision
- Rust Reference – Lifetime Elision Rules
- Rust API Guidelines – Lifetimes