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.
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.
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
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
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
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
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
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
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
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.
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
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.
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.
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.
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
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
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
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
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
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
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
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
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
- The Rust Book – Traits as Parameters
- The Rust Book – Trait Bounds
- Rust Reference – Trait Bounds
- Rust by Example – Bounds