Lifetime-Bounds sind Constraints im Typ-System, die Beziehungen zwischen Typen und Lifetimes oder zwischen Lifetimes selbst festlegen. T: 'a sagt: jede Referenz in T lebt mindestens 'a. 'a: 'b sagt: 'a lebt mindestens so lange wie 'b (lies: „a outlives b"). Diese Bounds sind in einfachen APIs selten — aber in Generic-Code mit Referenzen, in Library-Designs und beim Zusammenspiel mit Trait-Bounds tauchen sie regelmäßig auf. Wer sie versteht, kann komplexe Signaturen lesen und eigene flexible APIs bauen.
T: 'a — Typ outlives Lifetime
Der Bound T: 'a sagt: jede Referenz innerhalb von T lebt mindestens so lange wie 'a.
// Generic-Funktion mit Lifetime-Bound an T
fn store<'a, T: 'a>(x: T, slot: &'a mut Option<T>) {
*slot = Some(x);
}
fn main() {
let mut slot: Option<i32> = None;
store(42, &mut slot);
println!("{slot:?}");
}T: 'a sagt: T enthält keine geliehenen Daten, die kürzer leben als 'a. Damit ist garantiert, dass T sicher in &'a mut Option<T> gespeichert werden kann.
Wenn T owned ist (i32, String, Vec ohne Refs), ist T: 'a für jedes 'a automatisch erfüllt. Wenn T eine Referenz mit kürzerer Lifetime enthält, scheitert der Bound.
T: 'static — der häufigste Spezialfall
T: 'static ist der häufigste Lifetime-Bound. Er sagt: T enthält keine geliehenen Refs mit kurzer Lifetime — entweder owned, oder mit 'static-Refs.
use std::thread;
// thread::spawn fordert F: Send + 'static
fn spawn_print<T: std::fmt::Debug + Send + 'static>(value: T) {
thread::spawn(move || {
println!("{value:?}");
}).join().unwrap();
}
fn main() {
spawn_print(42); // OK
spawn_print(String::from("hello")); // OK
spawn_print(vec![1, 2, 3]); // OK
spawn_print("literal"); // OK — &'static str
}Mehr Details im Static-Lifetime-Artikel.
'a: 'b — Lifetime outlives Lifetime
Der Bound 'a: 'b sagt: 'a lebt mindestens so lange wie 'b. Damit darf eine Referenz mit 'a an Stellen verwendet werden, wo eine mit 'b erwartet wird.
// 'a muss mindestens so lange leben wie 'b
fn use_longer<'a: 'b, 'b>(long: &'a str, _short: &'b str) -> &'b str {
// Wir geben long zurück mit Lifetime 'b — funktioniert, weil 'a: 'b
long
}
fn main() {
let s1 = String::from("long-lived");
{
let s2 = String::from("short");
let r = use_longer(&s1, &s2); // Output hat Lifetime von s2
println!("{r}");
}
}Die Constraint 'a: 'b erlaubt dem Compiler zu verstehen: eine Ref mit 'a kann implizit zu einer Ref mit 'b werden (weil die längere Lifetime alle kürzeren umfasst).
Diese Outlives-Beziehungen heißen Subtyping auf Lifetime-Ebene — siehe Variance-Artikel für die Theorie dahinter.
Wann der Compiler Bounds verlangt
Manche Konstellationen lassen den Compiler nicht ableiten, dass Lifetime-Bounds erfüllt sind. Dann fordert er sie explizit.
// Generic-Struct mit Type-Parameter, der Lifetime enthält
struct Container<'a, T: 'a> {
data: &'a T,
}
// Ohne T: 'a würde der Compiler ablehnen — T könnte sonst kürzer leben als 'aWenn ein Generic-Struct eine Referenz auf T hält, muss der Compiler wissen, dass T mindestens so lange lebt wie die Referenz. Daher der Bound T: 'a.
In neueren Rust-Versionen ist diese Bound oft implizit — der Compiler fügt sie automatisch ein. Das nennt sich „implicit lifetime bounds". Wenn dir der Compiler eine Fehlermeldung „may not live long enough" gibt, ist meist ein expliziter Bound nötig.
Bounds in Trait-Definitionen
Lifetime-Bounds tauchen auch in Trait-Definitionen auf.
// Trait mit Lifetime-Parameter
pub trait Source<'a> {
type Output: 'a;
fn get(&'a self) -> Self::Output;
}type Output: 'a ist ein Bound am Associated Type. Implementierungen müssen einen Output-Typ wählen, der mindestens 'a lebt.
# pub trait Source<'a> {
# type Output: 'a;
# fn get(&'a self) -> Self::Output;
# }
struct StringSource {
data: String,
}
impl<'a> Source<'a> for StringSource {
type Output = &'a str; // Erfüllt 'a: 'a trivial
fn get(&'a self) -> &'a str {
&self.data
}
}Die Implementation wählt &'a str als Output — eine Referenz, die 'a lebt. Bound erfüllt.
where-Klauseln für komplexe Bounds
Bei komplexen Bound-Kombinationen ist die where-Klausel lesbarer.
use std::fmt::Display;
pub fn process<'a, 'b, T>(input: &'a T, label: &'b str) -> String
where
T: Display + 'a,
'a: 'b,
{
format!("[{label}] {input}")
}where T: Display + 'a, 'a: 'b kombiniert Trait-Bound und Lifetime-Bound. Inline wäre das in der Signatur überladen — where macht es übersichtlich.
Praxis: Lifetime-Bounds im echten Code
Thread-Helper mit Send + 'static
use std::thread;
use std::fmt::Debug;
pub fn spawn_with_log<T: Debug + Send + 'static>(label: &'static str, value: T) {
thread::spawn(move || {
println!("[{label}] {value:?}");
});
}
fn main() {
spawn_with_log("alpha", 42);
spawn_with_log("beta", vec![1, 2, 3]);
spawn_with_log("gamma", String::from("hi"));
}T: Send + 'static ist die typische Bound für Thread-Daten. Send wegen Thread-Übertragung, 'static wegen Lifetime-Garantie.
Generic-Container mit T: 'a
pub struct Wrapper<'a, T: 'a> {
inner: &'a T,
label: String,
}
impl<'a, T: 'a> Wrapper<'a, T> {
pub fn neu(inner: &'a T, label: impl Into<String>) -> Self {
Wrapper { inner, label: label.into() }
}
pub fn get(&self) -> &T {
self.inner
}
}
fn main() {
let value = 42;
let w = Wrapper::neu(&value, "answer");
println!("{}: {}", w.label, w.get());
}Generic-Struct mit Type-Parameter, dessen Lifetime explizit gebunden ist. In neueren Rust-Versionen oft implizit.
Callbacks mit 'static-Bound
pub struct Registry {
callbacks: Vec<Box<dyn Fn(i32) + Send + 'static>>,
}
impl Registry {
pub fn neu() -> Self {
Registry { callbacks: Vec::new() }
}
pub fn register<F: Fn(i32) + Send + 'static>(&mut self, f: F) {
self.callbacks.push(Box::new(f));
}
pub fn fire(&self, value: i32) {
for cb in &self.callbacks {
cb(value);
}
}
}
fn main() {
let mut reg = Registry::neu();
reg.register(|x| println!("got {x}"));
reg.register(|x| println!("doubled: {}", x * 2));
reg.fire(10);
}Callback-Registry mit 'static-Bound. Callbacks dürfen keine kurzlebigen Borrows enthalten — sonst wüsste der Compiler nicht, wie lange sie gültig bleiben.
Outlives in einer komplexen API
pub struct Cache<'long, 'short>
where 'long: 'short
{
primary: &'long str,
scratch: &'short str,
}
impl<'long, 'short> Cache<'long, 'short>
where 'long: 'short
{
pub fn neu(primary: &'long str, scratch: &'short str) -> Self {
Cache { primary, scratch }
}
// Output kann an 'short gebunden werden, weil 'long: 'short
pub fn current(&self) -> &'short str {
if self.scratch.is_empty() { self.primary } else { self.scratch }
}
}'long: 'short erlaubt, primary (mit 'long) an Stellen zu nutzen, wo 'short erwartet wird. Klassisch für Cache- oder Layered-Storage-APIs.
Iterator-Adapter mit Lifetime-Bound
pub fn take_while_borrowed<'a, T, P>(items: &'a [T], pred: P) -> Vec<&'a T>
where
P: Fn(&T) -> bool + 'a,
{
items.iter().take_while(|x| pred(x)).collect()
}
fn main() {
let v = vec![1, 2, 3, 4, 5];
let result = take_while_borrowed(&v, |&x| x < 4);
println!("{result:?}"); // [1, 2, 3]
}P: Fn(...) + 'a — die Closure muss mindestens 'a leben, damit sie über die Iterator-Pipeline hinweg gültig bleibt.
Box mit Lifetime-Constraint
use std::fmt::Display;
pub struct Holder<'a> {
inner: Box<dyn Display + 'a>,
}
impl<'a> Holder<'a> {
pub fn neu<T: Display + 'a>(value: T) -> Self {
Holder { inner: Box::new(value) }
}
pub fn print(&self) {
println!("{}", self.inner);
}
}
fn main() {
let local = String::from("local value");
let h = Holder::neu(&local); // T = &String, mit kurzer Lifetime
h.print();
}Box<dyn Display + 'a> mit expliziter Lifetime erlaubt borrowed Daten. Der Bound T: Display + 'a sorgt für die Lifetime-Garantie.
Bound an Trait-Methode
pub trait Storage<'a> {
fn get(&self, key: &str) -> Option<&'a str>;
fn put<T: AsRef<str> + 'a>(&mut self, key: String, value: T);
}Method-spezifische Lifetime-Bounds in Trait-Definitionen. Hier muss T mindestens 'a leben, damit es als Storage-Wert eingehängt werden kann.
Interessantes
T: 'a = T hat keine Refs kürzer als 'a.
Owned-Typen erfüllen das trivial. Typen mit Refs müssen Refs mit mindestens 'a-Lifetime haben.
T: 'static = T hat keine Refs mit nicht-statischer Lifetime.
Häufigster Spezialfall. Klassisch in Thread- und Async-APIs (thread::spawn, tokio::spawn).
'a: 'b = 'a outlives 'b.
'a lebt mindestens so lange wie 'b. Damit kann eine Ref mit 'a an Stellen mit 'b stehen. Subtyping auf Lifetime-Ebene.
Implizite Bounds — Compiler fügt sie oft ein.
In modernen Rust-Versionen sind viele Lifetime-Bounds implizit. Wenn Compiler-Meldung „may not live long enough" kommt, ist ein expliziter Bound nötig.
where-Klauseln für komplexe Bounds.
Bei mehreren Trait- und Lifetime-Bounds wird Inline unübersichtlich. where-Klausel sammelt alles am Ende, oft besser lesbar.
Bounds in Trait-Definitionen — Associated Types brauchen oft Bounds.
type Output: 'a zwingt Implementierungen, einen Output-Typ mit mindestens 'a-Lifetime zu wählen.
Box — explizite Trait-Object-Lifetime.
Default ist 'static. Für borrowed Daten musst du explizit eine andere Lifetime angeben.
Bounds drücken Garantien aus — keine Performance-Magie.
Lifetime-Bounds sind Compile-Zeit-Constraints. Sie haben keinen Runtime-Effekt, sondern lehren den Compiler, dass eine Sache sicher ist.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Generic Type Parameters, Trait Bounds, and Lifetimes Together
- Rust Reference – Type Parameter Bounds
- Rust Reference – Lifetime Bounds