Ein Typ-Parameter ohne Trait-Bound ist fast nutzlos: du kannst den Wert weiterreichen, aber praktisch nichts mit ihm anfangen. Erst die Trait-Bound — der Constraint, dass T einen bestimmten Trait implementieren muss — gibt dir Zugriff auf konkrete Methoden und Operationen. Mit T: PartialOrd kannst du vergleichen, mit T: Clone klonen, mit T: Display formatieren. Dieser Artikel zeigt die Syntax in allen Varianten, die wichtigsten Stdlib-Traits als Bounds, die kombinierte Form mit +, und die impl Trait-Syntax als Kurzform für einfache Fälle.

Die Grundsyntax

Trait-Bounds werden direkt am Typ-Parameter angegeben.

Rust Bound
fn groesstes<T: PartialOrd>(items: &[T]) -> &T {
    let mut groesstes = &items[0];
    for item in items {
        if item > groesstes {
            groesstes = item;
        }
    }
    groesstes
}

fn main() {
    let zahlen = [10, 25, 3, 42];
    let words = ["apple", "banana"];
    assert_eq!(*groesstes(&zahlen), 42);
    assert_eq!(*groesstes(&words), "banana");
}

<T: PartialOrd> sagt: „T ist ein Typ, der PartialOrd implementiert". Innerhalb der Funktion darfst du dann alle Methoden und Operatoren verwenden, die PartialOrd definiert — <, <=, >, >=, partial_cmp. Der Compiler prüft beim Aufruf, ob der konkrete Typ den Trait wirklich implementiert; sonst Compile-Fehler.

Ohne diesen Bound würde der Code nicht kompilieren: if item > groesstes ist nur erlaubt, wenn der Compiler weiß, dass T vergleichbar ist. Mit reinem <T> (ohne Bound) wäre die Vergleichs-Operation undefiniert.

Kombinierte Bounds mit +

Manchmal braucht ein Typ-Parameter mehrere Traits gleichzeitig.

Rust Mehrere Bounds
use std::fmt::Display;

fn beschreibe<T: Display + Clone>(x: T) -> String {
    let kopie = x.clone();
    format!("Wert: {x} (Kopie: {kopie})")
}

fn main() {
    let s = beschreibe(42);
    println!("{s}");        // "Wert: 42 (Kopie: 42)"
}

T: Display + Clone bedeutet: T muss sowohl Display als auch Clone implementieren. Beide Capabilities sind dann im Funktions-Body verfügbar — du kannst sowohl {x} formatieren als auch x.clone() aufrufen.

Die +-Syntax kombiniert beliebig viele Traits. Bei drei oder vier Bounds wird die Inline-Schreibweise schnell unleserlich — für komplexe Fälle gibt es die where-Klausel als Alternative (siehe eigener Artikel).

Verschiedene Parameter, verschiedene Bounds

Rust Pro Parameter
use std::fmt::Display;
use std::ops::Add;

fn paar_summe<T: Display, U: Add<Output = U> + Copy>(name: T, a: U, b: U) -> U {
    println!("Berechne Summe für {name}");
    a + b
}

fn main() {
    let s = paar_summe("Test", 10, 20);
    assert_eq!(s, 30);
}

Mehrere Typ-Parameter können jeweils eigene Bounds haben. T: Display für den Namen, U: Add<Output = U> + Copy für die Zahl. Die Bounds gelten unabhängig — T und U können verschiedene Typen sein.

Die wichtigsten Stdlib-Traits als Bounds

Bestimmte Traits tauchen in Trait-Bounds immer wieder auf. Es lohnt sich, sie und ihre Bedeutung zu kennen.

Clone und Copy

Rust Klonen
fn duplizieren<T: Clone>(x: T) -> (T, T) {
    (x.clone(), x)
}

fn kopie_machen<T: Copy>(x: T) -> (T, T) {
    (x, x)        // Copy passiert implizit
}

Clone erlaubt explizites .clone(). Wann immer du den Wert duplizieren willst — etwa weil zwei Stellen ihn unabhängig nutzen sollen —, brauchst du diesen Bound. Bei Heap-Typen wie String oder Vec ist .clone() eine echte Speicher-Allocation.

Copy ist stärker: der Typ darf bit-weise kopiert werden, ohne dass eine explizite Methode aufgerufen wird. Copy impliziert Clone. Für trivial-kopierbare Typen wie i32, f64, bool, char ist Copy der idiomatische Bound — Heap-Typen können Copy nicht implementieren.

Debug und Display

Rust Formatierung
use std::fmt::{Debug, Display};

fn debug_print<T: Debug>(x: T) {
    println!("{x:?}");
}

fn display_print<T: Display>(x: T) {
    println!("{x}");
}

Debug ist für Entwickler-Output gedacht. Die Stdlib-Form mit #[derive(Debug)] reicht meist; das {:?}-Format-Specifier zeigt die interne Struktur. Bei Logging-Code, Error-Handling, Test-Assertions ist dieser Bound üblich.

Display ist für menschen-lesbare Ausgabe. Wird mit {} formatiert. Anders als Debug musst du Display immer manuell implementieren — kein Derive. Für End-User-facing Code (CLI-Output, HTTP-Responses) ist Display der richtige Bound.

PartialEq und Eq

Rust Gleichheit
fn enthaelt<T: PartialEq>(slice: &[T], target: &T) -> bool {
    slice.iter().any(|x| x == target)
}

// Für HashMap-Keys: PartialEq + Eq + Hash
use std::collections::HashSet;
use std::hash::Hash;

fn dedupe<T: PartialEq + Eq + Hash + Clone>(items: &[T]) -> Vec<T> {
    let set: HashSet<T> = items.iter().cloned().collect();
    set.into_iter().collect()
}

PartialEq erlaubt == und !=. Bei den meisten Vergleichsoperationen ist das ausreichend.

Eq ist der Marker-Trait für „totale Gleichheit" — er sagt, dass die Equality reflexiv ist (x == x immer true). Bei Floats nicht erfüllt (NaN). Wird in Verbindung mit Hash für HashMap/HashSet gebraucht.

PartialOrd und Ord

Rust Ordnung
fn min<T: PartialOrd>(a: T, b: T) -> T {
    if a < b { a } else { b }
}

fn sortieren<T: Ord>(v: &mut Vec<T>) {
    v.sort();
}

PartialOrd erlaubt <, <=, >, >=. Bei den meisten Vergleichen reicht das.

Ord ist die totale Ordnung — alle Werte sind paarweise vergleichbar. Bei Floats nicht erfüllt. Wird für Vec::sort() und BTreeMap-Keys gebraucht.

Default

Rust Default-Wert
fn neu_oder_default<T: Default>(o: Option<T>) -> T {
    o.unwrap_or_default()
}

Default liefert einen Standard-Wert für den Typ. i32::default() = 0, String::default() = "", Vec::default() = vec![]. Sehr nützlich in Generic-Code, der „leere Anfangs-Werte" braucht.

Send und Sync

Rust Thread-Safety
use std::thread;

fn spawn_und_warte<T: Send + 'static>(data: T) {
    let handle = thread::spawn(move || {
        drop(data);     // Verwendet data im neuen Thread
    });
    handle.join().unwrap();
}

Send markiert, dass der Typ über Thread-Grenzen verschoben werden darf.

Sync markiert, dass der Typ über Referenzen aus mehreren Threads gelesen werden darf (&T ist Send).

Beide sind Auto-Traits — der Compiler implementiert sie automatisch, wenn alle Felder sie haben. Du brauchst sie selten explizit als Bound, außer in Multi-Thread-APIs.

impl Trait — die Kurzform

Für einfache Bounds gibt es eine kompaktere Syntax: impl Trait.

Rust impl Trait als Parameter
use std::fmt::Display;

// Lange Form
fn drucke_lang<T: Display>(x: T) {
    println!("{x}");
}

// Kurzform mit impl Trait
fn drucke_kurz(x: impl Display) {
    println!("{x}");
}

impl Display in einer Parameter-Position ist syntaktischer Zucker für einen anonymen Typ-Parameter mit Display-Bound. Der Compiler infert intern einen T: Display-Parameter.

Wann ist welche Form sinnvoll? impl Trait ist kompakter und gut für einfache Bounds an einer Funktion. <T: Trait> ist nötig, wenn du den Typ-Parameter mehrfach verwenden willst (etwa für Rückgabetyp), oder mehrere kombinierte Constraints hast.

impl Trait als Rückgabe-Typ

Rust impl Trait als Return
fn make_closure() -> impl Fn(i32) -> i32 {
    |x| x * 2
}

fn main() {
    let f = make_closure();
    assert_eq!(f(5), 10);
}

impl Fn(i32) -> i32 als Rückgabe-Typ ist die einzige Möglichkeit, Closures aus Funktionen zurückzugeben. Closure-Typen sind anonym — du kannst sie nicht direkt benennen. Mit impl Trait sagst du dem Compiler: „der Rückgabe-Typ ist irgendein Typ, der dieses Trait implementiert".

Diese Form ist auch nützlich für Iterator-Pipelines, wo der konkrete Typ (Map<Filter<...>>) sehr verschachtelt wäre — impl Iterator<Item = i32> ist viel lesbarer.

Trait-Bounds bei Structs und Enums

Auch Structs können Bounds an ihren Typ-Parametern haben.

Rust Struct mit Bound
use std::fmt::Display;

struct Logger<T: Display> {
    wert: T,
}

impl<T: Display> Logger<T> {
    fn loggen(&self) {
        println!("LOG: {}", self.wert);
    }
}

struct Logger<T: Display> führt einen Type-Parameter mit Bound ein — nur Display-fähige Typen können in einem Logger landen.

Eine wichtige Stilfrage: Bounds an Structs oder erst an impl-Blöcken? Die Rust-Community hat sich auf eine Konvention geeinigt: Bounds gehören meist an die impl-Blöcke, nicht an den Struct selbst.

Rust Bessere Variante
use std::fmt::Display;

// Struct OHNE Bound
struct Logger<T> {
    wert: T,
}

// Bound nur im relevanten impl-Block
impl<T: Display> Logger<T> {
    fn loggen(&self) {
        println!("LOG: {}", self.wert);
    }
}

// Andere impl-Blöcke können andere Bounds haben
impl<T> Logger<T> {
    fn neu(wert: T) -> Self {
        Logger { wert }
    }
}

Die Variante mit Bound nur am impl-Block ist flexibler: der Konstruktor neu funktioniert für alle Typen, aber loggen nur für Display-fähige. Bei der Struct-Variante wäre auch neu auf Display-Typen beschränkt — was unnötig restriktiv ist.

Static vs. Dynamic Dispatch

Generic-Funktionen mit Trait-Bounds nutzen Static Dispatch — der Compiler erzeugt für jeden konkreten Typ eine eigene Version (Monomorphisierung).

Es gibt eine Alternative: Dynamic Dispatch über dyn Trait.

Rust Static vs. Dynamic
use std::fmt::Display;

// Static Dispatch (Generic)
fn drucke_static<T: Display>(x: T) {
    println!("{x}");
}

// Dynamic Dispatch (dyn Trait)
fn drucke_dynamic(x: &dyn Display) {
    println!("{x}");
}

fn main() {
    drucke_static(42);
    drucke_dynamic(&42);
}

Die zwei Varianten haben unterschiedliche Performance- und Flexibilitäts-Charakteristiken:

Static Dispatch (<T: Trait>): der Compiler kennt den konkreten Typ zur Compile-Zeit und kann den Methoden-Call direkt einbauen (inline). Schnellster Code, aber jede konkrete Variante wird separat kompiliert (größere Binaries).

Dynamic Dispatch (&dyn Trait): der konkrete Typ ist erst zur Laufzeit bekannt. Methoden-Calls gehen über eine Vtable (Lookup-Tabelle). Etwas langsamer (eine Indirection), dafür ein einziger kompilierter Code-Pfad. Ermöglicht heterogene Sammlungen wie Vec<Box<dyn Trait>>.

In Performance-kritischem Code ist Static der Default. Dynamic ist sinnvoll, wenn du wirklich verschiedene Typen in einer Collection brauchst oder wenn die Code-Größe ein Problem ist.

Praxis: Trait-Bounds im echten Code

Numerische Helper

Rust Generic Math
use std::ops::{Add, Mul};

fn quadrat<T: Mul<Output = T> + Copy>(x: T) -> T {
    x * x
}

fn summe<T: Add<Output = T> + Copy + Default>(items: &[T]) -> T {
    let mut s = T::default();
    for x in items { s = s + *x; }
    s
}

fn main() {
    assert_eq!(quadrat(5), 25);
    assert_eq!(quadrat(2.5), 6.25);
    assert_eq!(summe(&[1, 2, 3, 4, 5]), 15);
    assert_eq!(summe(&[1.5_f64, 2.5, 3.0]), 7.0);
}

Generic über numerische Typen mit Operator-Traits. Add<Output = T> sagt: die Add-Operation ergibt wieder ein T. Copy erlaubt das mehrfache Verwenden des Werts ohne Move. Default::default() produziert das neutrale Element (0 für Zahlen).

Sort mit Custom-Key

Rust Sort by Key
fn sortiere_by_key<T, K: Ord, F: Fn(&T) -> K>(items: &mut [T], schluessel: F) {
    items.sort_by(|a, b| {
        let ka = schluessel(a);
        let kb = schluessel(b);
        ka.cmp(&kb)
    });
}

fn main() {
    let mut v = vec!["aaa", "b", "cc"];
    sortiere_by_key(&mut v, |s| s.len());
    assert_eq!(v, vec!["b", "cc", "aaa"]);
}

Generic über drei Typ-Parameter: T ist der Item-Typ (ohne Bound), K ist der Sortier-Schlüssel (mit Ord-Bound), F ist die Closure-Funktion. Das Pattern zeigt, wie man komplexe Generic-Signaturen sinnvoll aufbaut.

Validierter Konstruktor

Rust Validation mit Generic
use std::fmt::Debug;

pub struct Validated<T> {
    wert: T,
}

impl<T: PartialOrd + Debug> Validated<T> {
    pub fn neu(wert: T, min: T, max: T) -> Result<Self, String> {
        if wert < min || wert > max {
            return Err(format!("{wert:?} außerhalb von [{min:?}, {max:?}]"));
        }
        Ok(Validated { wert })
    }

    pub fn get(&self) -> &T {
        &self.wert
    }
}

fn main() {
    let v = Validated::neu(5, 1, 10).unwrap();
    assert_eq!(v.get(), &5);
    assert!(Validated::neu(50, 1, 10).is_err());
}

Validierung mit zwei Bounds: PartialOrd für den Range-Check, Debug für die Fehler-Nachricht. Der Wrapper-Typ macht garantierte Aussagen über den Inhalt.

Iterator-Adapter

Rust Custom Adapter
fn double_alle<I, T>(iter: I) -> impl Iterator<Item = T>
where
    I: Iterator<Item = T>,
    T: std::ops::Mul<Output = T> + Copy + From<i32>,
{
    iter.map(|x| x * T::from(2))
}

fn main() {
    let v: Vec<i32> = double_alle(vec![1, 2, 3].into_iter()).collect();
    assert_eq!(v, vec![2, 4, 6]);
}

Generic über Iterator und Item-Typ. Mehrere Bounds an T für numerische Operationen. impl Iterator<Item = T> als Rückgabe macht den konkreten Map-Iterator-Typ anonym.

Hash-basierter Cache

Rust HashMap-Wrapper
use std::collections::HashMap;
use std::hash::Hash;

pub struct Cache<K: Hash + Eq, V: Clone> {
    data: HashMap<K, V>,
}

impl<K: Hash + Eq, V: Clone> Cache<K, V> {
    pub fn neu() -> Self {
        Cache { data: HashMap::new() }
    }

    pub fn get_or_insert<F: FnOnce() -> V>(&mut self, key: K, default: F) -> V {
        self.data.entry(key).or_insert_with(default).clone()
    }
}

Klassischer Cache mit Multi-Param-Bounds. K braucht Hash + Eq für HashMap, V braucht Clone weil die Methode den Wert zurückgibt. Die Closure F ist mit FnOnce constrained.

Display-Wrapper

Rust Pretty-Print
use std::fmt::Display;

pub struct Boxed<T: Display>(pub T);

impl<T: Display> Display for Boxed<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "┌──────────────┐\n│ {} │\n└──────────────┘", self.0)
    }
}

fn main() {
    let b = Boxed(42);
    println!("{b}");
}

Newtype-Wrapper mit Display-Bound. Funktioniert mit jedem Display-fähigen Inhalt — Zahlen, Strings, eigene Typen.

Bedingte impl basierend auf Bounds

Rust Conditional Implementation
use std::fmt::Display;

pub struct Wrapper<T> {
    value: T,
}

impl<T> Wrapper<T> {
    pub fn new(value: T) -> Self {
        Wrapper { value }
    }
}

// Methode nur für Display-fähige Typen
impl<T: Display> Wrapper<T> {
    pub fn print(&self) {
        println!("{}", self.value);
    }
}

// Methode nur für klonbare Typen
impl<T: Clone> Wrapper<T> {
    pub fn duplikat(&self) -> Self {
        Wrapper { value: self.value.clone() }
    }
}

Verschiedene Methoden für verschiedene Bound-Sets. Konsumenten bekommen genau die Methoden, die ihr konkreter Typ unterstützt — kein erzwungener Bound auf der ganzen Struct-Definition.

impl Trait für Iterator-Rückgabe

Rust Iterator-Return
fn gerade_zahlen(max: u32) -> impl Iterator<Item = u32> {
    (0..max).filter(|n| n % 2 == 0)
}

fn main() {
    let v: Vec<u32> = gerade_zahlen(10).collect();
    assert_eq!(v, vec![0, 2, 4, 6, 8]);
}

impl Iterator<Item = u32> als Rückgabe-Typ verbirgt den komplexen internen Typ (Filter<Range<u32>, _>). Lesbarer und stabiler — wenn die Implementation sich ändert, bleibt die Signatur gleich.

Interessantes

Bounds geben dir Capabilities.

Ohne Bound kannst du fast nichts mit T machen. Jeder Bound fügt eine Capability hinzu — Clone erlaubt .clone(), PartialOrd erlaubt </>, Display erlaubt {}-Formatierung. Bounds sind das, was Generic-Code überhaupt nützlich macht.

Kombiniere Bounds mit +.

T: Display + Clone + PartialOrd ist die Standard-Syntax für mehrere Constraints. Bei drei oder mehr Bounds wird die Inline-Form unleserlich — dann wechsle zur where-Klausel.

Copy ist stärker als Clone.

Jeder Copy-Typ ist auch Clone — Copy: Clone ist eine Trait-Hierarchie. Wer Copy als Bound nutzt, bekommt auch Clone gratis. Aber: Heap-Typen wie String und Vec können nicht Copy sein, also schließt der Bound sie aus.

impl Trait als Parameter — Kurzform für einfache Bounds.

fn foo(x: impl Display) ist äquivalent zu fn foo<T: Display>(x: T). Kompakter, aber du verlierst die Möglichkeit, den Parameter im Body als Typ-Namen zu nutzen. Bei mehreren Verwendungen lieber die explizite Form.

impl Trait als Return — verbirgt den konkreten Typ.

Bei Iterator-Pipelines oder Closures unverzichtbar. Der Compiler kennt den konkreten Typ intern, aber Konsumenten sehen nur den Trait. Macht die API stabiler gegen Implementations-Änderungen.

Bounds an impl-Blöcke, nicht an Struct-Definitionen.

Konvention: Struct-Definitionen haben keinen Bound (struct Foo<T>), die Bounds gehören in die impl<T: Bound>-Blöcke. So bleibt der Typ flexibel und kann verschiedene impl-Blöcke mit verschiedenen Constraints haben.

Static Dispatch (Generic) ist schneller als Dynamic (dyn).

Generic-Funktionen werden für jeden konkreten Typ monomorphisiert — der Compiler kann inline-en, optimieren. dyn Trait geht über Vtable, was eine Indirection kostet. In Performance-kritischem Code ist Generic der Default.

Send/Sync sind Auto-Traits.

Du implementierst sie selten manuell. Der Compiler markiert Typen automatisch als Send/Sync, wenn alle Felder es sind. Als Bound brauchst du sie in Multi-Thread-APIs (tokio, thread::spawn).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Generics

Zur Übersicht