Die meisten Funktions-Signaturen brauchen dank Elision-Regeln keine expliziten Lifetimes. Aber sobald eine Funktion mehrere Referenz-Inputs hat und einen davon (oder eine Ableitung davon) zurückgibt, muss der Programmierer klären, welche Input-Lifetime an den Output gebunden ist. Dieser Artikel zeigt systematisch die Fälle: einfache Funktionen, Funktionen mit zwei oder mehr Inputs, die Bedeutung der Output-Lifetime, und wie sich der Programmierer entscheidet, welche Annotation richtig ist.

Eine Referenz rein, eine raus

Der einfachste Fall: ein Input, ein Output. Hier braucht es keine explizite Annotation — die Elision-Regeln greifen.

Rust Single-Input
// Ohne Annotation — Elision macht alles
fn ersten_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

// Explizit annotiert — semantisch identisch
fn ersten_word_explizit<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

Bei einer einzelnen Input-Referenz ist klar: der Output kann nur an diesen einen Input gebunden sein. Der Compiler weist beiden dieselbe Lifetime zu — keine Annotation nötig.

Mehrere Inputs — Ausgabe an einen gebunden

Hier wird es interessant. Bei mehreren Input-Referenzen muss der Programmierer dem Compiler sagen, an welchen Input der Output gebunden ist.

Rust Mehrere Inputs, Output an ersten gebunden
fn first_input<'a>(primary: &'a str, _secondary: &str) -> &'a str {
    primary
}

fn main() {
    let s1 = String::from("primary");
    let r;
    {
        let s2 = String::from("secondary");
        r = first_input(&s1, &s2);
        // r zeigt auf s1, nicht auf s2 — keine Bindung an 'b (oder kürzere)
    }                                  // s2 dropped — kein Problem
    println!("{r}");                    // OK: r zeigt auf s1
}

Hier ist nur primary mit 'a annotiert, _secondary hat eine elidierte Lifetime (vom Compiler abgeleitet, vom Programmierer nicht benannt). Der Output ist an 'a gebunden — also an die Lebenszeit von primary. Die Lebenszeit von _secondary spielt für den Output keine Rolle.

Output an beide Inputs gebunden

Wenn der Output je nach Logik aus beiden Inputs stammen kann, müssen sie eine gemeinsame Lifetime teilen.

Rust Output aus beiden möglich
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
}

longest deklariert nur eine Lifetime 'a, der beide Inputs UND der Output zugewiesen sind. Der Compiler wählt 'a so, dass es kompatibel zu beiden Inputs ist — typischerweise als die kürzere der beiden tatsächlichen Lifetimes. Damit ist garantiert: egal welcher Input zurückgegeben wird, der Output ist gültig.

Wie der Compiler die Lifetime wählt

Bei einem Funktions-Aufruf nimmt der Compiler die Lifetime-Parameter und füllt sie mit konkreten Lifetimes auf — er findet die kleinste passende Wahl.

Rust Lifetime-Inferenz
# 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("short");      // s1: 'scope_main
    let result;

    {
        let s2 = String::from("longer");  // s2: 'scope_inner
        result = longest(&s1, &s2);
        // Beim Aufruf:
        //   &s1 hat Lifetime 'scope_main
        //   &s2 hat Lifetime 'scope_inner
        //   'a muss in beiden enthalten sein → 'a = 'scope_inner (die kürzere)
        //   Output hat Lifetime 'scope_inner
        println!("{result}");              // OK: noch in 'scope_inner
    }                                       // s2 dropped, 'scope_inner endet
    // result darf hier nicht mehr genutzt werden
}

Die Inferenz wählt die kleinste Lifetime, die alle Bedingungen erfüllt. Wenn das nicht möglich ist (z.B. Output muss länger leben als ein Input), gibt es einen Compile-Fehler.

Output-Lifetime aus Input ableiten

Eine Funktion gibt häufig eine Sub-Referenz zurück — einen Slice in einen Input-String, einen Pointer in ein Input-Array.

Rust Slice-Returns
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' { return &s[..i]; }
    }
    s
}

fn main() {
    let text = String::from("hello world");
    let word = first_word(&text);
    println!("{word}");        // "hello"
    // word lebt nur so lange wie text
}

Output ist eine Sub-Slice in den Input. Lifetime des Outputs gleich Lifetime des Inputs — Elision regelt das automatisch.

Funktionen mit mut-Referenz

Mutable Referenzen funktionieren analog. Der Compiler erlaubt nur eine mut-Referenz gleichzeitig (Borrow-Regel) — das wirkt mit der Lifetime zusammen.

Rust Mut-Ref-Return
fn first_mut<'a>(v: &'a mut Vec<i32>) -> &'a mut i32 {
    &mut v[0]
}

fn main() {
    let mut v = vec![1, 2, 3];
    let first = first_mut(&mut v);
    *first = 100;
    // v.push(4);     // FEHLER: v ist noch mutable-borrowed via first
    println!("{first}");
    // ab hier endet first's Lifetime (NLL)
    v.push(4);          // OK
}

Während first aktiv ist, ist v mutable-borrowed. Erst wenn first nicht mehr genutzt wird (NLL), kann v wieder operativ genutzt werden.

Praxis: typische Funktions-Patterns

Suche in Sammlung

Rust Search
fn find_in<'a>(haystack: &'a [String], needle: &str) -> Option<&'a String> {
    haystack.iter().find(|s| s.contains(needle))
}

fn main() {
    let items = vec![
        String::from("apple"),
        String::from("banana"),
        String::from("cherry"),
    ];
    let found = find_in(&items, "an");
    println!("{found:?}");        // Some("banana")
}

Klassisches Pattern: Suche in einer Sammlung, Rückgabe einer Referenz auf das gefundene Element. Output an haystack gebunden, needle hat eigene unabhängige Lifetime.

Tokenizer

Rust Tokenizer
fn tokenize<'a>(source: &'a str) -> Vec<&'a str> {
    source.split_whitespace().collect()
}

fn main() {
    let text = String::from("hello world rust");
    let tokens = tokenize(&text);
    for t in &tokens {
        println!("{t}");
    }
}

Vec von Referenzen in den Source-String. Die Tokens leben so lange wie der Source — keine Allocation für jeden Token. Sehr effizient für Parser-Code.

Wahl zwischen zwei Inputs

Rust Choose
fn pick<'a>(prefer: &'a str, fallback: &'a str) -> &'a str {
    if !prefer.is_empty() { prefer } else { fallback }
}

Funktion gibt eines von zwei Inputs zurück — beide brauchen dieselbe Lifetime.

Output an einen, Logging mit dem anderen

Rust Asymmetric
fn extract<'a>(source: &'a str, label: &str) -> &'a str {
    println!("[{label}] extracting from len {}", source.len());
    &source[..10.min(source.len())]
}

fn main() {
    let data = String::from("12345678901234567890");
    let head;
    {
        let log_label = String::from("temp");
        head = extract(&data, &log_label);
        // log_label kann dropped — head ist nicht daran gebunden
    }
    println!("{head}");
}

Asymmetrische Lifetimes: source ist mit 'a annotiert (gebunden an Output), label hat eigene Lifetime. Real-World-Beispiel — viele APIs nehmen Konfig-Strings, die nur lokal gebraucht werden.

Mehrere Output-Refs

Rust Multi-Output
fn split_word(s: &str) -> (&str, &str) {
    let mid = s.find(' ').unwrap_or(s.len());
    let head = &s[..mid];
    let tail = &s[mid.saturating_add(1)..];
    (head, tail)
}

fn main() {
    let text = "hello world rust";
    let (first, rest) = split_word(text);
    println!("first={first}, rest={rest}");
}

Tuple-Output mit zwei Referenzen — beide elidiert vom einzigen Input.

Iterator-Adapter

Rust Filter mit Lifetime
fn long_words<'a>(text: &'a str, min_len: usize) -> Vec<&'a str> {
    text.split_whitespace()
        .filter(|w| w.len() >= min_len)
        .collect()
}

fn main() {
    let text = String::from("the quick brown fox jumps over the lazy dog");
    let longs = long_words(&text, 4);
    println!("{longs:?}");        // ["quick", "brown", "jumps"]
}

Filter, der Sub-Slices auswählt. Output-Vec hält Referenzen in den Input — alle mit derselben Lifetime.

Cross-Function Borrowing

Rust Borrow-Kette
fn process<'a>(input: &'a str) -> String {
    let trimmed = trim_input(input);
    let cleaned = clean_input(trimmed);
    cleaned.to_string()
}

fn trim_input(s: &str) -> &str {
    s.trim()
}

fn clean_input(s: &str) -> &str {
    s.trim_start_matches(|c: char| !c.is_alphanumeric())
}

Lifetime-Verkettung über mehrere Funktionen. Jede Hilfsfunktion gibt eine Sub-Slice zurück, die Top-Level-Funktion baut am Ende einen owned String.

Interessantes

Single-Input + Single-Output braucht keine Annotation.

Elision-Regel: bei einem Input wird dessen Lifetime an alle Output-Referenzen weitergegeben. Keine 'a-Syntax nötig.

Mehrere Inputs erzwingen Annotation, wenn Output ein Ref ist.

Der Compiler weiß nicht, welcher Input für den Output verantwortlich ist. Du musst klären: Output an Input A, an Input B, oder an beide?

Gleiche Lifetime auf mehreren Inputs = kürzere wird gewählt.

fn foo<'a>(a: &'a, b: &'a) — die kürzer-lebende Input-Lifetime bestimmt 'a. Der Output ist an diese Mindestlebenszeit gebunden.

Verschiedene Lifetimes = unabhängige Inputs.

fn foo<'a, 'b>(a: &'a, b: &'b) -> &'a — Output nur an 'a gebunden. b kann andere Lebensdauer haben, ohne den Output zu beeinflussen.

Output-Lifetime entscheidet, wie lange der Aufrufer das Ergebnis nutzen darf.

Wenn der Output an die kürzeste Input-Lifetime gebunden ist, ist er nur so lange gültig. Das ist eine API-Design-Entscheidung mit Konsequenzen.

Tuple-Outputs verteilen Lifetimes auf alle Elemente.

fn split(s: &str) -> (&str, &str) — beide Outputs erben die Input-Lifetime. Elision-Regel greift weiterhin.

Mut-Ref-Funktionen: nur eine mut-Ref aktiv.

Während die Output-Mut-Ref lebt, ist der Input mutable-gesperrt. Erst nach dem letzten Use (NLL) ist der Input wieder frei.

Bei Unsicherheit: explizit annotieren.

Wenn dir die Elision-Regeln nicht klar sind oder die Signatur komplex ist, schreib alle Lifetimes explizit aus. Das ist nie falsch und macht die Bindings klar.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Lifetimes

Zur Übersicht