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.
// 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.
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.
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.
# 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.
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.
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
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
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
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
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
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
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
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
- The Rust Book – Lifetimes in Function Signatures
- Rust Reference – Lifetime Elision in Functions
- Rust by Example – Functions with Lifetimes