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").

Rust Lifetime-Subtyping
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 von Vec<&'short str>? — Ja
  • &'a mut &'long str ein 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 von Wrapper<'short>, wenn 'long: 'short.
  • Kontravariant: Wrapper kehrt die Beziehung um. Wrapper<'short> ist Subtyp von Wrapper<'long>. Selten.
  • Invariant: Wrapper hat keine Subtyp-Beziehung. Wrapper<'long> und Wrapper<'short> sind unrelated.

Kovarianz — die Regel-Variante

Die meisten Typen sind kovariant bzgl. ihrer Lifetime-Parameter.

Rust Kovariante Typen
// &'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 T
  • Vec<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.

Rust Invariante mutable Refs
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 werden

Warum 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 T
  • Cell<T> — invariant in T
  • RefCell<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.

Rust Kontravariante Argumente
// 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.

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

Rust Stdlib-Variance
// &'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 T

Das ist die Standard-Variance-Tabelle der wichtigsten Stdlib-Typen. Wer sie kennt, weiß sofort, welche Lifetime-Konvertierungen erlaubt sind.

Funktion, die kovariante Refs akzeptiert

Rust Kovariante Akzeptanz
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

Rust Cell-Invarianz
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

Rust Vec-Kovarianz
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

Rust Subtyping in Action
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

Rust Custom-Iter
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

Rust Invarianter Wrapper
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

Rust Verkürzung
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

/ Weiter

Zurück zu Lifetimes

Zur Übersicht