Ein Associated Type ist ein Typ, der zur Trait-Implementation gehört. Anders als ein Generic-Parameter ist er pro Implementation fest gewählt — Iterator for Vec<i32> hat genau einen Item-Typ (i32), nicht beliebig viele. Diese Festlegung macht APIs lesbarer und Type-Inferenz robuster. Der Stdlib-Iterator-Trait ist die Wegweiser-Anwendung: type Item; als Associated Type bedeutet, jeder Iterator-Typ entscheidet, welchen Item-Typ er produziert, und alle Methoden-Signaturen können Self::Item referenzieren.
Syntax
Innerhalb eines Trait-Bodies wird ein Associated Type mit dem type-Keyword deklariert.
trait Container {
type Item; // Associated Type, von Implementierern zu liefern
fn get(&self, idx: usize) -> Option<&Self::Item>;
fn add(&mut self, item: Self::Item);
}type Item; ist eine Deklaration ohne konkreten Typ — der Implementierer muss den Typ festlegen. Im Trait-Body wird er als Self::Item referenziert.
# trait Container {
# type Item;
# fn get(&self, idx: usize) -> Option<&Self::Item>;
# fn add(&mut self, item: Self::Item);
# }
struct StringContainer {
items: Vec<String>,
}
impl Container for StringContainer {
type Item = String; // Item-Typ festlegen
fn get(&self, idx: usize) -> Option<&String> {
self.items.get(idx)
}
fn add(&mut self, item: String) {
self.items.push(item);
}
}Im impl-Block wird type Item = String; festgelegt. Ab da steht in allen Methoden-Signaturen Self::Item für String.
Associated Type vs. Generic-Parameter
Auf den ersten Blick kann man sich fragen: warum nicht einfach Generic-Parameter? Der Unterschied ist subtil aber wichtig.
// Mit Generic-Parameter
trait ContainerG<T> {
fn get(&self, idx: usize) -> Option<&T>;
fn add(&mut self, item: T);
}
// Erlaubt mehrere Implementierungen für einen Typ:
struct Multi;
impl ContainerG<String> for Multi { /* ... */ }
impl ContainerG<i32> for Multi { /* ... */ } // Auch erlaubt// Mit Associated Type
trait ContainerA {
type Item;
fn get(&self, idx: usize) -> Option<&Self::Item>;
fn add(&mut self, item: Self::Item);
}
// Nur EINE Implementation pro Typ:
struct Single;
impl ContainerA for Single {
type Item = String;
// ...
}
// impl ContainerA for Single { type Item = i32; ... } // FEHLER: schon impl'dDer zentrale Unterschied:
- Generic-Parameter: ein Typ kann mehrere Implementations des Traits haben (eine pro Parameter-Belegung)
- Associated Type: ein Typ kann nur eine Implementation haben (mit einem festgelegten Associated Type)
Daher gilt die Faustregel: Associated Type wenn pro Typ nur eine sinnvolle Implementation existiert, Generic-Parameter wenn mehrere sinnvoll sind.
Stdlib-Beispiele:
Iterator { type Item; }— ein Iterator hat einen Item-Typ, keine zwei.From<T> for U— eine Konvertierung pro (Quell-Ziel)-Paar; passt zu Generic.
Mehrere Associated Types
Ein Trait kann beliebig viele Associated Types haben.
trait Storage {
type Key;
type Value;
type Error;
fn get(&self, key: &Self::Key) -> Result<Self::Value, Self::Error>;
fn put(&mut self, key: Self::Key, value: Self::Value) -> Result<(), Self::Error>;
}
struct MemoryStorage {
data: std::collections::HashMap<String, Vec<u8>>,
}
impl Storage for MemoryStorage {
type Key = String;
type Value = Vec<u8>;
type Error = String;
fn get(&self, key: &String) -> Result<Vec<u8>, String> {
self.data.get(key).cloned().ok_or_else(|| String::from("not found"))
}
fn put(&mut self, key: String, value: Vec<u8>) -> Result<(), String> {
self.data.insert(key, value);
Ok(())
}
}Drei Associated Types — Key, Value, Error — gemeinsam festgelegt. Andere Implementations könnten andere Typen wählen (MemoryStorage<u64, String> etc.).
Bounds an Associated Types
Du kannst Bounds direkt an Associated Types in Trait-Deklarationen setzen.
use std::fmt::Display;
trait DisplayContainer {
type Item: Display; // Bound am Associated Type
fn drucke_alle(&self);
}
struct StringList(Vec<String>);
impl DisplayContainer for StringList {
type Item = String; // String ist Display — OK
fn drucke_alle(&self) {
for s in &self.0 {
println!("{s}");
}
}
}type Item: Display; erzwingt: jede Implementation muss einen Display-fähigen Item-Typ wählen. Alternative wäre where Self::Item: Display in den Methoden, aber an der Trait-Deklaration ist es klarer.
use std::hash::Hash;
use std::fmt::Debug;
trait Cache {
type Key: Hash + Eq + Clone;
type Value: Clone + Debug;
fn lookup(&self, key: &Self::Key) -> Option<Self::Value>;
}+-Kombination wie überall sonst in Bounds. Implementierungen müssen alle Bedingungen erfüllen.
Iterator<Item = T> in Bounds
Beim Verwenden eines Traits mit Associated Type kannst du den Type-Wert in der Bound festlegen.
// Akzeptiert NUR Iteratoren mit Item = i32
fn summe_i32<I: Iterator<Item = i32>>(iter: I) -> i32 {
iter.sum()
}
// Akzeptiert NUR Iteratoren mit Item = String
fn drucke_strings<I: Iterator<Item = String>>(iter: I) {
for s in iter {
println!("{s}");
}
}
// Generic über Item-Typ
fn count<I: Iterator>(iter: I) -> usize {
iter.count()
}Die Iterator<Item = i32>-Syntax sagt: jeder Typ I, der Iterator implementiert UND dessen Item gleich i32 ist. Das ist ein Equality-Constraint auf den Associated Type — Stdlib-Pattern, sehr verbreitet.
use std::fmt::Display;
// Iterator mit beliebigem Item, das aber Display sein muss
fn drucke<I>(iter: I)
where
I: Iterator,
I::Item: Display, // Bound auf Associated Type
{
for x in iter {
println!("{x}");
}
}I::Item: Display ist ein Bound auf den Associated Type — funktioniert in where-Klauseln und ist die klare Variante, wenn du den Item-Typ nicht festlegst, aber Constraints stellst.
Default-Werte für Associated Types
Seit Rust 1.x (experimentell, in der Stdlib genutzt) können Associated Types Default-Werte haben — Stand 2026 stabil für einige Cases, instabil für andere.
// Stdlib: std::ops::Add
// pub trait Add<Rhs = Self> {
// type Output;
// fn add(self, rhs: Rhs) -> Self::Output;
// }
// Default für Rhs ist Self → Add für selbst-gleich-typ Addition
// impl Add for i32 — Rhs default = i32
// impl Add<f64> for i32 — explizit andere RhsIm Trait-Deklaration ist Rhs = Self ein Generic-Parameter mit Default-Wert. Bei Associated Types ist die analoge Funktionalität teilweise via RFC stabilisiert; die häufigste Form bleibt: Generic-Parameter mit Default.
Praxis: Associated Types im echten Code
Iterator (Stdlib)
// Vereinfachte Stdlib-Definition:
pub trait MeinIterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
pub struct Counter { count: u32 }
impl MeinIterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut c = Counter { count: 0 };
while let Some(v) = c.next() {
println!("{v}");
}
}Der Iterator-Trait ist das Paradebeispiel für Associated Types. Jeder Iterator hat genau einen Item-Typ — nicht mehrere.
Repository-Pattern
pub trait Repository {
type Entity;
type Id;
type Error;
fn find(&self, id: Self::Id) -> Result<Self::Entity, Self::Error>;
fn save(&mut self, entity: Self::Entity) -> Result<Self::Id, Self::Error>;
}
struct User { name: String }
struct UserRepo {
users: Vec<User>,
}
impl Repository for UserRepo {
type Entity = User;
type Id = usize;
type Error = String;
fn find(&self, id: usize) -> Result<User, String> {
self.users.get(id)
.map(|u| User { name: u.name.clone() })
.ok_or_else(|| format!("User {id} not found"))
}
fn save(&mut self, user: User) -> Result<usize, String> {
self.users.push(user);
Ok(self.users.len() - 1)
}
}Repository-Trait mit Entity, Id und Error als Associated Types. Pro Repository festgelegt — UserRepo hat keine zwei verschiedenen Entity-Typen.
Future-Trait (vereinfacht)
// Stdlib: std::future::Future hat ähnliche Form
pub trait MyFuture {
type Output; // Was die Future am Ende liefert
fn poll(&mut self) -> Option<Self::Output>;
}
struct DelayedValue<T> { value: Option<T> }
impl<T> MyFuture for DelayedValue<T> {
type Output = T;
fn poll(&mut self) -> Option<T> {
self.value.take()
}
}Future hat einen Output-Typ als Associated Type — was beim Auflösen herauskommt.
Parser-Trait
pub trait Parser {
type Input;
type Output;
type Error;
fn parse(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
}
struct JsonParser;
impl Parser for JsonParser {
type Input = String;
type Output = serde_json::Value;
type Error = serde_json::Error;
fn parse(&self, input: String) -> Result<serde_json::Value, serde_json::Error> {
serde_json::from_str(&input)
}
}Parser mit Input/Output/Error als Associated Types. Jeder Parser-Typ legt seine drei Typen fest — pro Parser nur eine sinnvolle Belegung.
Service-Trait für DI
pub trait Service {
type Request;
type Response;
fn call(&self, request: Self::Request) -> Self::Response;
}
struct EchoService;
impl Service for EchoService {
type Request = String;
type Response = String;
fn call(&self, request: String) -> String {
request
}
}
// Verwender mit Bound-Konfiguration:
fn handle<S: Service<Request = String, Response = String>>(s: &S, msg: String) -> String {
s.call(msg)
}Service-Trait mit Request und Response als Associated Types. Konsumenten können entweder generisch (S: Service) oder mit Equality-Constraint (Service<Request = String, Response = String>) arbeiten.
Iterator-Adapter
pub struct Skip<I> {
iter: I,
n: usize,
}
impl<I: Iterator> Iterator for Skip<I> {
type Item = I::Item; // Item-Typ vom Wrapped-Iterator übernehmen
fn next(&mut self) -> Option<I::Item> {
while self.n > 0 {
self.n -= 1;
self.iter.next()?;
}
self.iter.next()
}
}Iterator-Adapter mit Item = I::Item — der Adapter übernimmt den Item-Typ des umschlossenen Iterators. Klassisches Stdlib-Pattern.
Bound-Kombination
use std::fmt::Debug;
// Akzeptiert Iteratoren mit Debug-fähigem Item-Typ
pub fn drucke_debug<I>(iter: I)
where
I: IntoIterator,
I::Item: Debug,
{
for x in iter {
println!("{x:?}");
}
}
fn main() {
drucke_debug(vec![1, 2, 3]);
drucke_debug(vec!["a", "b"]);
drucke_debug([(1, "x"), (2, "y")]);
}Bound an Associated Type über die where-Klausel. Die Funktion akzeptiert jeden IntoIterator, dessen Items Debug sind — sehr generisch.
Interessantes
Associated Type = pro Implementation EIN Typ.
type Item; im Trait-Body deklariert, type Item = String; in der Implementation festgelegt. In Signaturen via Self::Item referenziert.
Unterschied zu Generic-Parameter: Eindeutigkeit.
Generic-Parameter erlauben mehrere Implementationen pro Typ (impl Trait<i32> for Foo, impl Trait<String> for Foo). Associated Type erlaubt nur EINE Implementation pro Typ.
Faustregel: type wenn pro Typ eine sinnvolle Variante.
Iterator hat einen Item-Typ, kein zwei — Associated Type. From<T> kann pro Ziel-Typ mehrere Quell-Typen haben — Generic-Parameter. Stdlib-Konventionen folgen dieser Heuristik.
Iterator in Bounds — Equality-Constraint.
Beim Verwenden des Traits kannst du den Associated Type festlegen: I: Iterator<Item = i32> akzeptiert nur Iteratoren über i32. Sehr verbreitetes Stdlib-Pattern.
Bounds direkt am Associated Type — type Item: Display.
Im Trait-Body am Associated Type stellbar. Implementations müssen einen Display-fähigen Typ wählen. Alternative: where Self::Item: Display an den Methoden.
Mehrere Associated Types pro Trait — kein Problem.
type Key; type Value; type Error; zusammen deklariert, in der Implementation gemeinsam festgelegt. Pattern für Repository, Storage, Parser, Service.
Self::Type für Self-Referenzen.
Im Trait-Body und in Impl-Blöcken referenzierst du Associated Types als Self::Item. Außerhalb (in Bounds anderer Funktionen) als <T as Trait>::Item oder via Pfad-Syntax.
Associated Types machen APIs lesbarer.
Iterator::Item ist klarer als Iterator<Item> mit Generic-Parameter. Type-Inferenz funktioniert robuster, weil pro Typ nur ein Item-Wert möglich ist.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Specifying Placeholder Types with Associated Types
- Rust Reference – Associated Types
- std::iter::Iterator
- std::future::Future