dyn Trait ist Rusts Mechanismus für runtime-Polymorphismus. Statt dass der Compiler zur Compile-Zeit für jeden konkreten Typ Code erzeugt (wie bei Generics), wird hier zur Laufzeit über eine Vtable entschieden, welche Implementation aufzurufen ist. Das ermöglicht Dinge, die mit Generics nicht gehen: heterogene Sammlungen (Vec<Box<dyn Animal>> mit verschiedenen Tieren), Plugin-Architekturen mit zur Laufzeit gewählten Implementierungen, und APIs, deren konkreter Typ erst zur Laufzeit feststeht. Der Preis: ein indirekter Aufruf statt direktem, und der Trait muss object-safe sein.

Was ein Trait-Objekt ist

Ein Trait-Objekt ist ein Wert, dessen konkreter Typ zur Laufzeit unbekannt ist — bekannt ist nur, dass er ein bestimmtes Trait implementiert.

Rust Erstes Trait-Objekt
trait Tier {
    fn laut(&self) -> String;
}

struct Hund;
impl Tier for Hund {
    fn laut(&self) -> String { String::from("Wuff!") }
}

struct Katze;
impl Tier for Katze {
    fn laut(&self) -> String { String::from("Miau!") }
}

fn main() {
    // Vector mit Trait-Objekten — verschiedene Typen!
    let tiere: Vec<Box<dyn Tier>> = vec![
        Box::new(Hund),
        Box::new(Katze),
        Box::new(Hund),
    ];

    for tier in &tiere {
        println!("{}", tier.laut());
    }
}

Box<dyn Tier> ist ein Trait-Objekt: ein Box-allozierter Wert, dessen Typ irgendetwas ist, das Tier implementiert. Der Compiler weiß zur Compile-Zeit nicht, ob da Hund oder Katze drinsteckt — er erzeugt Code, der zur Laufzeit über eine Vtable die richtige Methode auswählt.

Das ist nicht mit Generics zu erreichen: Vec<T> where T: Tier würde alle Elemente vom gleichen Typ T erzwingen. Mit Vec<Box<dyn Tier>> darf jedes Element ein anderer Typ sein, solange alle Tier implementieren.

Die Vtable

Hinter dyn Trait steckt eine technische Datenstruktur: die Vtable (Virtual Table). Sie ist eine Tabelle mit Funktions-Pointern zu den Methoden des Typs.

Rust Konzeptuelle Darstellung
Box<dyn Tier> intern:

[ data_pointer  →  Heap-Wert (Hund-Instanz) ]
[ vtable_pointer →  Tier-Vtable für Hund      ]

Tier-Vtable für Hund:
[ drop()         →  Hund::drop                 ]
[ size           →  size_of::<Hund>            ]
[ align          →  align_of::<Hund>           ]
[ laut()         →  <Hund as Tier>::laut       ]

Ein Trait-Objekt-Wert besteht aus zwei Pointern: einer auf die Daten, einer auf die Vtable. Daher heißt es auch Fat Pointer: doppelt so groß wie ein normaler Pointer.

Bei einem Methoden-Aufruf wie tier.laut() macht der Compiler:

  1. Schaue in die Vtable
  2. Hole den Funktions-Pointer für laut
  3. Rufe diese Funktion mit dem data_pointer als self

Das ist ein indirekter Aufruf — eine zusätzliche Indirektion gegenüber direktem Funktions-Aufruf. Auf modernen CPUs sind das wenige Nanosekunden, aber Inlining und Spezialisierung sind nicht möglich.

Drei Trägerformen

Trait-Objekte können auf verschiedene Weisen gespeichert werden — je nach Owner-Semantik.

Rust Box, Referenz, Rc
# trait Tier { fn laut(&self) -> String; }
# struct Hund;
# impl Tier for Hund { fn laut(&self) -> String { String::from("Wuff") } }
use std::rc::Rc;

// 1. Box<dyn Trait> — heap-alloziert, owned
let b: Box<dyn Tier> = Box::new(Hund);
println!("{}", b.laut());

// 2. &dyn Trait — geliehene Referenz
let h = Hund;
let r: &dyn Tier = &h;
println!("{}", r.laut());

// 3. Rc<dyn Trait> — reference-counted, shared ownership
let rc: Rc<dyn Tier> = Rc::new(Hund);
println!("{}", rc.laut());
  • Box<dyn Tier> — owned, heap-alloziert. Klassisch für Container und Plugin-Slots.
  • &dyn Tier — geliehen, kein heap-Speicher nötig. Klassisch für Funktions-Parameter.
  • Rc<dyn Tier> / Arc<dyn Tier> — geteiltes Eigentum. Klassisch für Plugin-Systeme mit Shared-State.

Alle drei sind Fat-Pointer und gehen über Vtable. Die Wahl hängt nur am Ownership-Modell.

Object-Safety

Nicht jeder Trait kann als dyn Trait verwendet werden. Der Trait muss object-safe sein.

Ein Trait ist object-safe, wenn alle seine Methoden mit Trait-Objekten kompatibel sind. Die wichtigsten Regeln:

  1. Keine generic Methoden — Methoden mit eigenen Type-Parametern (fn foo<T>(&self, x: T)) sind verboten. Grund: jede generische Instantiierung würde eine eigene Vtable-Slot brauchen.
  2. Kein Self als Rückgabe-Typ — Methoden wie fn neu() -> Self funktionieren nicht. Bei einem dyn Trait weiß der Compiler nicht, welcher konkrete Typ zurückgegeben werden soll.
  3. Kein Self: Sized-Methoden direkt verwendbar — Methoden mit where Self: Sized werden im Trait-Objekt-Kontext einfach ausgeblendet (aber sind erlaubt).
Rust Object-Safe
trait Sichtbar {
    // OK: keine generic Methoden, kein Self im Rückgabe-Typ
    fn anzeigen(&self) -> String;
    fn breite(&self) -> u32;
}

// Funktioniert als dyn Trait:
fn render(item: &dyn Sichtbar) {
    println!("{} (Breite: {})", item.anzeigen(), item.breite());
}
Rust Nicht object-safe
trait Builder {
    // Self-Rückgabe — Trait ist NICHT object-safe
    fn neu() -> Self;
}

// Compile-Fehler:
// fn use_builder(b: &dyn Builder) { ... }
//                  ^^^^^^^^^^^ the trait `Builder` cannot be made into an object
Rust Mit Sized-Escape-Hatch
trait Container {
    // Diese Methode ist im dyn-Trait-Kontext verfügbar
    fn count(&self) -> usize;

    // Diese ist mit Sized markiert — nur bei konkreten Typen verfügbar
    fn into_vec(self) -> Vec<u8> where Self: Sized;
}

// OK — Trait ist object-safe, weil into_vec mit Sized ausgeblendet wird
fn use_container(c: &dyn Container) {
    println!("{}", c.count());     // OK
    // c.into_vec();                // Compile-Fehler: nicht für dyn
}

Der where Self: Sized-Trick ist klassisch: Methoden, die Self als Wert nehmen oder zurückgeben, werden so markiert. Der Trait bleibt object-safe, die Methoden sind nur für konkrete Typen verfügbar.

dyn vs. Generic — der direkte Vergleich

Rust Generic — Static Dispatch
# trait Tier { fn laut(&self) -> String; }
// Mit Generic: Compiler erzeugt für jeden Typ eigene Variante (Monomorphization)
fn lautstark<T: Tier>(tier: &T) {
    println!("LAUT: {}", tier.laut());
}
Rust dyn — Dynamic Dispatch
# trait Tier { fn laut(&self) -> String; }
// Mit dyn: eine einzige Funktion, runtime-Lookup via Vtable
fn lautstark(tier: &dyn Tier) {
    println!("LAUT: {}", tier.laut());
}

Beide Varianten sehen ähnlich aus, sind aber grundlegend verschieden:

AspektGenericdyn
DispatchCompile-ZeitLaufzeit
Code-GrößeEine Variante pro TypEine Variante insgesamt
PerformanceInlining möglichIndirekter Aufruf
Heterogene SammlungNicht möglichMöglich
Object-Safety nötigNeinJa

Mehr Details im nächsten Artikel.

dyn als Rückgabe-Typ

Funktionen können Box<dyn Trait> zurückgeben — sehr nützlich für Plugin-Systeme.

Rust Factory-Pattern
# trait Tier { fn laut(&self) -> String; }
# struct Hund;
# impl Tier for Hund { fn laut(&self) -> String { String::from("Wuff") } }
# struct Katze;
# impl Tier for Katze { fn laut(&self) -> String { String::from("Miau") } }

// Erzeugt ein Tier — Typ wird zur Laufzeit gewählt
fn erzeuge_tier(art: &str) -> Box<dyn Tier> {
    match art {
        "hund" => Box::new(Hund),
        "katze" => Box::new(Katze),
        _ => Box::new(Hund),
    }
}

fn main() {
    let t1 = erzeuge_tier("hund");
    let t2 = erzeuge_tier("katze");
    println!("{}", t1.laut());
    println!("{}", t2.laut());
}

Mit Box<dyn Tier> als Rückgabe-Typ kann die Funktion verschiedene konkrete Typen zurückgeben — abhängig vom Input. Mit impl Tier ginge das nicht: dort muss der Rückgabe-Typ über alle Pfade derselbe konkrete Typ sein.

Praxis: dyn Trait im echten Code

Plugin-Architektur

Rust Plugin-System
pub trait Plugin {
    fn name(&self) -> &str;
    fn execute(&self, input: &str) -> String;
}

pub struct UppercasePlugin;
impl Plugin for UppercasePlugin {
    fn name(&self) -> &str { "uppercase" }
    fn execute(&self, input: &str) -> String {
        input.to_uppercase()
    }
}

pub struct ReversePlugin;
impl Plugin for ReversePlugin {
    fn name(&self) -> &str { "reverse" }
    fn execute(&self, input: &str) -> String {
        input.chars().rev().collect()
    }
}

pub struct PluginRegistry {
    plugins: Vec<Box<dyn Plugin>>,
}

impl PluginRegistry {
    pub fn neu() -> Self { Self { plugins: Vec::new() } }

    pub fn register(&mut self, plugin: Box<dyn Plugin>) {
        self.plugins.push(plugin);
    }

    pub fn process(&self, input: &str) -> Vec<String> {
        self.plugins.iter()
            .map(|p| format!("{}: {}", p.name(), p.execute(input)))
            .collect()
    }
}

fn main() {
    let mut reg = PluginRegistry::neu();
    reg.register(Box::new(UppercasePlugin));
    reg.register(Box::new(ReversePlugin));
    for result in reg.process("hello") {
        println!("{result}");
    }
}

Klassische Plugin-Architektur: das PluginRegistry hält eine Liste heterogener Plugins. Neue Plugins können zur Laufzeit registriert werden — der Code im Registry kennt ihre konkreten Typen nicht.

Event-Handler-System

Rust Event-Handler
pub trait EventHandler {
    fn handle(&self, event: &str);
}

struct Logger;
impl EventHandler for Logger {
    fn handle(&self, event: &str) {
        println!("LOG: {event}");
    }
}

struct AlertSystem;
impl EventHandler for AlertSystem {
    fn handle(&self, event: &str) {
        if event.contains("ERROR") {
            println!("ALERT: {event}");
        }
    }
}

struct EventBus {
    handlers: Vec<Box<dyn EventHandler>>,
}

impl EventBus {
    fn neu() -> Self { Self { handlers: Vec::new() } }

    fn subscribe(&mut self, h: Box<dyn EventHandler>) {
        self.handlers.push(h);
    }

    fn publish(&self, event: &str) {
        for h in &self.handlers {
            h.handle(event);
        }
    }
}

fn main() {
    let mut bus = EventBus::neu();
    bus.subscribe(Box::new(Logger));
    bus.subscribe(Box::new(AlertSystem));
    bus.publish("App gestartet");
    bus.publish("ERROR: Verbindung verloren");
}

Event-Bus mit heterogenen Handlern. Logger und AlertSystem sind verschiedene Typen — würden aber im Vec<Box<dyn EventHandler>> nebeneinander stehen.

GUI-Widget-Tree

Rust Widget-Tree
pub trait Widget {
    fn render(&self) -> String;
}

struct Label { text: String }
impl Widget for Label {
    fn render(&self) -> String {
        format!("Label: {}", self.text)
    }
}

struct Button { caption: String }
impl Widget for Button {
    fn render(&self) -> String {
        format!("[Button: {}]", self.caption)
    }
}

struct Panel {
    children: Vec<Box<dyn Widget>>,
}

impl Widget for Panel {
    fn render(&self) -> String {
        let inner: Vec<String> = self.children.iter()
            .map(|c| c.render())
            .collect();
        format!("Panel({})", inner.join(", "))
    }
}

fn main() {
    let panel = Panel {
        children: vec![
            Box::new(Label { text: String::from("Name") }),
            Box::new(Button { caption: String::from("OK") }),
        ],
    };
    println!("{}", panel.render());
}

Klassischer Widget-Tree: ein Panel enthält heterogene Children. Ohne dyn Widget müsste der Tree alle Children vom gleichen Typ haben — nicht praktikabel.

Strategy-Pattern

Rust Strategy
pub trait CompressionStrategy {
    fn compress(&self, data: &[u8]) -> Vec<u8>;
    fn name(&self) -> &str;
}

struct GzipStrategy;
impl CompressionStrategy for GzipStrategy {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        // Vereinfachung: tatsächliche gzip-Logik
        data.to_vec()
    }
    fn name(&self) -> &str { "gzip" }
}

struct ZstdStrategy;
impl CompressionStrategy for ZstdStrategy {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        data.to_vec()
    }
    fn name(&self) -> &str { "zstd" }
}

pub struct Compressor {
    strategy: Box<dyn CompressionStrategy>,
}

impl Compressor {
    pub fn neu(strategy: Box<dyn CompressionStrategy>) -> Self {
        Self { strategy }
    }

    pub fn run(&self, data: &[u8]) -> Vec<u8> {
        println!("Komprimiere mit {}", self.strategy.name());
        self.strategy.compress(data)
    }
}

fn main() {
    let c = Compressor::neu(Box::new(ZstdStrategy));
    c.run(b"hello world");
}

Strategy-Pattern: die Kompressions-Strategie wird zur Konstruktor-Zeit gewählt. Konfiguration aus einer Config-Datei könnte zwischen Strategien wählen, ohne dass der Compressor-Typ generisch sein muss.

Filter-Pipeline

Rust Filter-Chain
pub trait Filter {
    fn apply(&self, input: &str) -> String;
}

struct TrimFilter;
impl Filter for TrimFilter {
    fn apply(&self, input: &str) -> String {
        input.trim().to_string()
    }
}

struct LowercaseFilter;
impl Filter for LowercaseFilter {
    fn apply(&self, input: &str) -> String {
        input.to_lowercase()
    }
}

struct ReplaceFilter { from: String, to: String }
impl Filter for ReplaceFilter {
    fn apply(&self, input: &str) -> String {
        input.replace(&self.from, &self.to)
    }
}

pub struct Pipeline {
    filters: Vec<Box<dyn Filter>>,
}

impl Pipeline {
    pub fn neu() -> Self { Self { filters: Vec::new() } }

    pub fn add(mut self, f: Box<dyn Filter>) -> Self {
        self.filters.push(f);
        self
    }

    pub fn process(&self, input: &str) -> String {
        self.filters.iter()
            .fold(input.to_string(), |acc, f| f.apply(&acc))
    }
}

fn main() {
    let pipeline = Pipeline::neu()
        .add(Box::new(TrimFilter))
        .add(Box::new(LowercaseFilter))
        .add(Box::new(ReplaceFilter {
            from: String::from(" "),
            to: String::from("_"),
        }));

    let result = pipeline.process("  Hello World  ");
    assert_eq!(result, "hello_world");
}

Filter-Chain: heterogene Filter werden zur Laufzeit zu einer Pipeline kombiniert. Builder-Style API mit .add().

Interessantes

dyn Trait = runtime-polymorpher Wert.

Der konkrete Typ ist zur Compile-Zeit unbekannt; die Methoden-Auswahl läuft zur Laufzeit über eine Vtable.

Fat Pointer — zwei Pointer pro Trait-Objekt.

Ein Trait-Objekt-Wert besteht aus data-Pointer (auf den eigentlichen Wert) und vtable-Pointer (auf die Methoden-Tabelle). Daher doppelt so groß wie normale Pointer.

Häufige Container: Box, &dyn T, Rc.

Box für owned heap-Werte, & für geliehene Referenzen, Rc/Arc für geteiltes Eigentum. Wahl hängt am Ownership-Modell.

Heterogene Sammlungen — der Hauptgrund für dyn.

Vec<Box<dyn Trait>> darf verschiedene konkrete Typen halten. Mit Generic-Containern (Vec<T>) wäre das unmöglich — dort müssen alle Elemente vom gleichen Typ T sein.

Object-Safety — Trait muss bestimmte Regeln einhalten.

Keine generic Methoden, kein Self im Rückgabe-Typ, kein Self als Wert-Parameter. Sonst lehnt der Compiler dyn Trait ab. Workaround: where Self: Sized für problematische Methoden.

Performance-Kosten — moderat.

Indirekter Aufruf statt Inlining. Auf modernen CPUs wenige Nanosekunden. In Hot-Loops messbar, in normalen APIs irrelevant. Nutze dyn, wenn Heterogenität wichtiger ist als letzte Performance.

dyn als Rückgabe-Typ — Factory-Pattern.

fn factory() -> Box<dyn Trait> darf je nach Logik verschiedene konkrete Typen zurückgeben. Mit impl Trait als Rückgabe wäre das nicht möglich — dort muss der konkrete Typ über alle Pfade gleich sein.

dyn ist explizit — seit Edition 2018 Pflicht.

Frühe Rust-Versionen erlaubten Box<Trait> ohne dyn-Keyword. Seit Edition 2018 ist dyn Trait Pflicht — explizit kennzeichnet, dass es sich um runtime-Polymorphismus handelt.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Traits

Zur Übersicht