Das letzte Lifetime-Thema ist auch das tiefste: Variance. Dahinter steckt die Frage, wann der Compiler eine Lifetime stillschweigend zu einer anderen umwandeln darf. Wenn 'long länger lebt als 'short, darf eine &'long str als &'short str benutzt werden? — meist ja. Aber gilt das auch für &mut &'long str als &mut &'short str? — nein. Diese Unterscheidung heißt Variance, und jeder Typ-Konstruktor hat eine bestimmte Variance bzgl. seiner Type- und Lifetime-Parameter. Die meisten Rust-Programmierer brauchen dieses Detail-Wissen nur selten — aber wer sehr generische Library-APIs baut oder mit PhantomData und unsafe-Code arbeitet, kommt nicht drumherum.
Subtyping auf Lifetime-Ebene
Eine Lifetime 'long ist ein Subtyp der Lifetime 'short, wenn 'long mindestens so lange lebt wie 'short. Notation: 'long: 'short (lies: „long outlives short").
fn main() {
let long_lived = String::from("long"); // Lifetime: 'long
{
let short_lived = String::from("short"); // Lifetime: 'short
// 'long umfasst 'short — 'long ist Subtyp von 'short
let r_long: &str = &long_lived; // &'long str
let r_short: &str = r_long; // &'short str — implizite Konvertierung
println!("{r_short}");
}
}Die Zuweisung let r_short: &str = r_long; ist eine implizite Subtyp-Konvertierung: eine Referenz mit 'long-Lifetime wird als eine mit 'short behandelt. Das ist sicher, weil 'long ja umfasst 'short — die Referenz lebt noch länger als nötig.
Subtyping bedeutet hier: „du darfst eine längere Lifetime an Stellen verwenden, wo eine kürzere erwartet wird". Genau wie in OO-Sprachen, wo eine Subclass an Stelle einer Superclass stehen darf.
Variance — was passiert bei Wrapper-Typen?
Spannend wird es, wenn die Lifetime durch einen Wrapper-Typ wandert. Sagen wir, 'long ist Subtyp von 'short. Ist dann auch:
Vec<&'long str>ein Subtyp vonVec<&'short str>? — Ja&'a mut &'long strein Subtyp von&'a mut &'short str? — Nein!
Diese Unterschiede ergeben sich aus der Variance des Wrapper-Typs bzgl. seines Parameters. Drei Möglichkeiten:
- Kovariant: Wrapper bewahrt die Subtyp-Beziehung.
Wrapper<'long>ist Subtyp vonWrapper<'short>, wenn'long: 'short. - Kontravariant: Wrapper kehrt die Beziehung um.
Wrapper<'short>ist Subtyp vonWrapper<'long>. Selten. - Invariant: Wrapper hat keine Subtyp-Beziehung.
Wrapper<'long>undWrapper<'short>sind unrelated.
Kovarianz — die Regel-Variante
Die meisten Typen sind kovariant bzgl. ihrer Lifetime-Parameter.
// &'a T ist kovariant in 'a
fn use_short<'a>(r: &'a str) {
println!("{r}");
}
fn main() {
let s = String::from("hello");
let r: &'static str = "literal"; // 'static lebt länger als jeder andere Scope
use_short(r); // 'static → 'a — kovariant
}&'a T ist kovariant bzgl. 'a. Du darfst eine &'static str (längere Lifetime) an eine Funktion übergeben, die &'a str (kürzere Lifetime) erwartet.
Weitere kovariante Typen:
&'a T— kovariant in'a(und in T)Box<T>— kovariant in TVec<T>— kovariant in T*const T— kovariant- Funktion-Rückgabe-Typ — kovariant
Faustregel: wenn der Typ den Parameter lesend nutzt (nicht schreibend), ist er kovariant.
Invarianz — bei Mutation
Mutable Referenzen sind invariant bzgl. ihres Pointee-Typs.
fn make_invariant<'a>(r: &mut &'a str) {
let temp = String::from("temp");
// *r = &temp; // Würde dangling reference erzeugen, wenn erlaubt
let _ = r;
}
// Compiler erlaubt nicht:
// let mut s: &'static str = "hello";
// let r: &mut &'static str = &mut s;
// make_invariant(r); // wenn das 'a verkürzen würde, könnte *r = &temp gefährlich werdenWarum sind mutable Refs invariant? Weil sie sowohl lesen ALS AUCH schreiben können. Beim Schreiben könnte ein neuer Wert mit kürzerer Lifetime hineingelangen — das würde die ursprüngliche Garantie brechen.
Konkret: &'a mut T ist invariant in T. Daher ist &mut &'long str weder Subtyp noch Supertyp von &mut &'short str — sie sind komplett unrelated.
Invariante Typen:
&'a mut T— invariant in TCell<T>— invariant in TRefCell<T>— invariant in T*mut T— invariant- Funktion-Argument-Typ (siehe Kontravarianz)
Kontravarianz — selten
Kontravarianz ist die Umkehrung der Subtyp-Beziehung. Sie taucht in einem einzigen Fall auf: bei Funktions-Argument-Typen.
// fn(&'a T) -> R ist kontravariant in 'a
//
// Wenn 'long: 'short:
// fn(&'short str) ist Subtyp von fn(&'long str)
// (umgekehrte Richtung!)
//
// Bedeutung: eine Funktion, die mit kurzlebigen Strings umgehen kann,
// kann auch mit langlebigen umgehen.
fn accept_long(_: fn(&'static str)) {}
fn process<'a>(s: &'a str) { println!("{s}"); }
fn main() {
// process akzeptiert beliebig kurze Lifetimes
// accept_long erwartet einen fn(&'static str)
// Kontravarianz erlaubt die Konvertierung
// (Beispiel ist konstruiert — in der Praxis selten relevant)
}Kontravarianz tritt bei Funktions-Argument-Positionen auf. In der Praxis musst du dir das selten anschauen — der Compiler kümmert sich.
PhantomData und Variance
Wenn du eigene Typen mit Lifetime-Parametern baust, die den Parameter NICHT direkt in einem Feld nutzen, brauchst du PhantomData<...>, um dem Compiler die richtige Variance zu signalisieren.
use std::marker::PhantomData;
// Struct nutzt 'a nicht direkt — PhantomData markiert es kovariant
pub struct CovariantWrapper<'a, T> {
ptr: *const T,
_marker: PhantomData<&'a T>,
}
// Mit &'a mut T wird PhantomData invariant
pub struct InvariantWrapper<'a, T> {
ptr: *mut T,
_marker: PhantomData<&'a mut T>,
}PhantomData<...> erbt die Variance des Marker-Typs. Wenn das Marker-Type kovariant ist, wird der Struct kovariant. Wenn invariant — invariant.
Das ist wichtig bei eigenen Smart-Pointers, Iteratoren mit komplexen Internals, und überall, wo unsafe-Code mit Lifetimes spielt. Mehr dazu im fortgeschrittenen unsafe-Kapitel.
Praxis: Variance in der Wildbahn
Stdlib-Beispiele
// &'a T — kovariant in 'a und in T
// &'a mut T — kovariant in 'a, invariant in T
// Box<T> — kovariant in T
// Vec<T> — kovariant in T
// Cell<T> — invariant in T
// RefCell<T> — invariant in T
// fn(T) -> U — kontravariant in T, kovariant in U
// *const T — kovariant in T
// *mut T — invariant in TDas ist die Standard-Variance-Tabelle der wichtigsten Stdlib-Typen. Wer sie kennt, weiß sofort, welche Lifetime-Konvertierungen erlaubt sind.
Funktion, die kovariante Refs akzeptiert
fn use_str(s: &str) {
println!("{s}");
}
fn main() {
// 'static-Lifetime kann als kürzere durchgereicht werden
use_str("literal"); // OK: &'static → kürzere Funktion-Param-Lifetime
let owned = String::from("owned");
use_str(&owned); // OK: aktuelle Lifetime → genaue Param-Lifetime
}Kovarianz erlaubt: eine längere Lifetime wird zur kürzeren konvertiert, wo sie als Argument erwartet wird.
Cell als invariantes Beispiel
use std::cell::Cell;
fn use_cell<'a>(c: Cell<&'a str>) {
c.set("modified");
}
fn main() {
let cell: Cell<&'static str> = Cell::new("hello");
// use_cell(cell);
// FEHLER (konzeptionell): Cell ist invariant — &'static cannot be used as &'a
// Praktisch: Cell<&'static> != Cell<&'a> bei nicht-statischem 'a
let _ = cell;
}Cell ist invariant, weil sie Mutation erlaubt. Das verhindert subtile Bugs — du kannst keine Cell<&'static> an eine Funktion übergeben, die Cell<&'short> erwartet, weil die Funktion sonst einen &'short-Wert hineinsetzen könnte, der die ursprüngliche Garantie verletzt.
Vec kovariant in T
fn process_strs(items: Vec<&str>) {
for s in &items {
println!("{s}");
}
}
fn main() {
let static_items: Vec<&'static str> = vec!["a", "b", "c"];
process_strs(static_items); // OK: Vec kovariant
}Vec<&'static str> darf als Vec<&'short str> behandelt werden (kovariant). Das ist intuitiv — eine Liste von langlebigen Refs ist auch eine Liste von kurzlebigeren Refs.
Subtyping in einer Funktion
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let static_str: &'static str = "static";
{
let local = String::from("local");
// longest erwartet beide mit derselben Lifetime
// Compiler nimmt die kürzere ('local-scope)
// 'static wird zu dieser kürzeren Lifetime gemacht (kovariant)
let result = longest(static_str, &local);
println!("{result}");
}
}Hier wirkt Subtyping unsichtbar: static_str mit 'static-Lifetime wird zur kürzeren Lifetime des inneren Scopes konvertiert, damit beide Inputs dieselbe 'a haben.
PhantomData für eigene Iter-Typen
use std::marker::PhantomData;
pub struct MyIter<'a, T: 'a> {
ptr: *const T,
end: *const T,
_marker: PhantomData<&'a T>, // Kovariante Marker
}
impl<'a, T> Iterator for MyIter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<&'a T> {
if self.ptr == self.end { return None; }
// Vereinfachte Logik — echter Iter wäre komplexer
unsafe {
let item = &*self.ptr;
self.ptr = self.ptr.add(1);
Some(item)
}
}
}Custom-Iterator mit Raw-Pointer und PhantomData für die Lifetime-Markierung. So bekommt der Iterator die richtige Variance.
Wrapper mit invarianter Lifetime
use std::marker::PhantomData;
// Wrapper, der invariant in 'a sein soll
pub struct ExclusiveBorrow<'a, T> {
_data: *mut T,
_marker: PhantomData<&'a mut T>, // Invariante Marker
}Mit PhantomData<&'a mut T> wird der Wrapper invariant bzgl. 'a. Wichtig bei eigenen Smart-Pointer-Typen, die exklusiven Zugriff modellieren.
Lifetime-Verkürzung beim Aufruf
fn process_locally<'a>(s: &'a str) {
println!("{s}");
}
fn main() {
let static_s: &'static str = "static-string";
let owned = String::from("owned");
process_locally(static_s); // 'static → 'a (kovariant)
process_locally(&owned); // Lifetime von owned → 'a
}Beim Funktions-Aufruf wird 'a vom Compiler gewählt. Längere Input-Lifetimes werden via Kovarianz auf das gewählte 'a „verkürzt".
Interessantes
Subtyping: längere Lifetime kann als kürzere gelten.
'long: 'short heißt: 'long lebt mindestens so lange wie 'short. Damit darf eine Ref mit 'long an Stellen verwendet werden, wo 'short erwartet wird.
Drei Variance-Arten: kovariant, kontravariant, invariant.
Kovariant: bewahrt Subtyp-Beziehung. Kontravariant: kehrt sie um. Invariant: keine Beziehung. Jeder Typ-Konstruktor hat eine bestimmte Variance bzgl. seiner Parameter.
Kovariante Typen sind die Regel — lesender Zugriff.
&T, Box<T>, Vec<T>, *const T. Wenn der Typ den Parameter nur lesend nutzt, ist er kovariant. Längere Lifetime kann als kürzere durchgehen.
Invariant bei Mutation — &mut T, Cell.
Mutable Refs und Cells sind invariant in T. Sie können sowohl lesen als auch schreiben — kein Subtyp-Beziehungs-Schema funktioniert.
Kontravariant nur bei Funktion-Argumenten.
fn(T) ist kontravariant in T. Selten relevant — meist kümmert sich der Compiler.
PhantomData erbt Variance vom Marker-Typ.
Wichtig bei eigenen Typen, die einen Lifetime/Type-Parameter nicht direkt nutzen. PhantomData<&'a T> macht kovariant, PhantomData<&'a mut T> invariant, PhantomData<fn(T) -> ()> kontravariant.
Im Alltag merkst du Variance kaum.
Compiler kümmert sich. Du brauchst das Detail-Wissen nur bei sehr generischen Library-Designs, eigenen Smart-Pointers, und unsafe-Code.
Variance erklärt subtile Lifetime-Fehler.
Wenn der Compiler in einer scheinbar einfachen Funktion einen Lifetime-Fehler wirft, der mit Subtyping zu tun hat, ist die Variance-Erklärung die Antwort.
Weiterführende Ressourcen
Externe Quellen
- The Rustonomicon – Subtyping and Variance
- Rust Reference – Variance
- std::marker::PhantomData
- Common Rust Lifetime Misconceptions