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.
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.
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:
- Schaue in die Vtable
- Hole den Funktions-Pointer für
laut - 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.
# 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:
- 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. - Kein
Selfals Rückgabe-Typ — Methoden wiefn neu() -> Selffunktionieren nicht. Bei einemdyn Traitweiß der Compiler nicht, welcher konkrete Typ zurückgegeben werden soll. - Kein
Self: Sized-Methoden direkt verwendbar — Methoden mitwhere Self: Sizedwerden im Trait-Objekt-Kontext einfach ausgeblendet (aber sind erlaubt).
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());
}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 objecttrait 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
# 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());
}# 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:
| Aspekt | Generic | dyn |
|---|---|---|
| Dispatch | Compile-Zeit | Laufzeit |
| Code-Größe | Eine Variante pro Typ | Eine Variante insgesamt |
| Performance | Inlining möglich | Indirekter Aufruf |
| Heterogene Sammlung | Nicht möglich | Möglich |
| Object-Safety nötig | Nein | Ja |
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.
# 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
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
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
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
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
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
- The Rust Book – Trait Objects
- Rust Reference – Trait Objects
- Object Safety in Rust
- Huon Wilson – Peeking Inside Trait Objects