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.

Rust Regel 1
// 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.

Rust Regel 2
// 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 str

Bei einer einzelnen Input-Ref greift die Regel: der Output erbt die Input-Lifetime. Sehr häufiger Fall, deckt die meisten Slice-Funktionen ab.

Rust Regel 2 greift NICHT bei mehreren Inputs
// 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 specifier

Bei 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.

Rust Regel 3
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.

Rust Beispiele für scheiternde Elision
// 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.

Rust Elidiert vs. explizit
// 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

Rust Stdlib mit Elision
// 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

Rust Klare 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 str

Methode mit Regel 3

Rust Methode
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

Rust Manuelle Annotation nötig
// 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

Rust Hybrid
// 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

Rust Method mit zusätzlichen Refs
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

Rust Closures
// 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

/ Weiter

Zurück zu Lifetimes

Zur Übersicht