Bevor wir zu Trait-Bounds und komplexeren Generic-Konstrukten kommen, lohnt es sich, die reine Typ-Parameter-Syntax zu verstehen — also die <T>-Notation in ihrer einfachsten Form, ohne Constraints. Dieser Artikel zeigt, wie Typ-Parameter in Funktionen, Structs, Enums und impl-Blöcken deklariert werden, was die Konventionen für Parameter-Namen sind, wie mehrere Parameter zusammenarbeiten, und wie die Type-Inference den Compiler unterstützt, Generics bei jedem Aufruf automatisch aufzulösen.

Generische Funktionen

Die einfachste Form: eine Funktion mit einem Typ-Parameter.

Rust Generische Funktion
fn identitaet<T>(x: T) -> T {
    x
}

fn main() {
    let n = identitaet(42);          // T = i32
    let s = identitaet("Hallo");      // T = &str
    let v = identitaet(vec![1, 2]);   // T = Vec<i32>
    println!("{n} {s} {v:?}");
}

Die Syntax fn name<T>(...) führt den Typ-Parameter T ein. Die Parameter-Liste steht vor den normalen Funktions-Parametern, in spitzen Klammern. Im Funktions-Body und in den Parameter-Typen kannst du T verwenden, als wäre es ein konkreter Typ.

Beim Aufruf passiert die Type-Inference: der Compiler schaut, mit welchem Argument-Typ die Funktion aufgerufen wird, und setzt T entsprechend ein. Bei identitaet(42) ist das Argument ein i32, also wird T = i32. Bei identitaet("Hallo") ist es ein &str, also T = &str. Der Compiler erzeugt dann separate Maschinencode-Versionen für jeden konkreten Typ (mehr im Monomorphisierung-Artikel).

Wichtig: ein Typ-Parameter ist funktions-lokal — er existiert nur innerhalb der Funktion, an deren Signatur er deklariert wurde. Verschiedene Funktionen können ihre eigenen T-Parameter haben, die nichts miteinander zu tun haben.

Mehrere Typ-Parameter

Rust Zwei Parameter
fn paar<A, B>(a: A, b: B) -> (A, B) {
    (a, b)
}

fn main() {
    let p1 = paar(42, "Hallo");          // A=i32, B=&str
    let p2 = paar("x", vec![1, 2, 3]);    // A=&str, B=Vec<i32>
    println!("{p1:?} {p2:?}");
}

Mehrere Typ-Parameter werden mit Kommas getrennt. Sie sind unabhängig voneinander — A und B können gleich oder verschieden sein. Der Compiler infert sie unabhängig aus den Argumenten.

Konventionen für Parameter-Namen

Die Rust-Community hat sich auf bestimmte Buchstaben für typische Use-Cases geeinigt. Du musst sie nicht zwingend einhalten — aber wer sich daran hält, schreibt Code, der für andere Rust-Entwickler sofort vertraut wirkt.

BuchstabeVerwendung
TGenerischer Typ (Standard)
U, V, ...Weitere Typ-Parameter neben T
KKey-Typ (HashMap, BTreeMap)
VValue-Typ (HashMap, BTreeMap)
EError-Typ (Result)
A, B, CBei Tupeln oder funktionalen APIs
RReturn-Typ (selten, bei Closures)
F, GFunction/Closure-Typ
IIterator-Typ
SSelf/State (in Builder-Patterns)

Sehr kurze, ein-buchstabige Namen sind Standard, weil Typ-Parameter in den meisten Fällen T heißen können, ohne Verwirrung zu stiften. Bei semantisch bedeutsamen Typen wirst du aber auch längere Namen sehen — Item, Output, Inner sind in der Stdlib häufige Beispiele.

Rust Stdlib-Beispiele
// Aus der Stdlib (vereinfacht):
// pub struct HashMap<K, V> { /* ... */ }
// pub enum Result<T, E> { Ok(T), Err(E) }
// pub trait Iterator { type Item; /* ... */ }
// pub trait FnOnce<Args> { type Output; /* ... */ }

HashMap<K, V> nutzt die K/V-Konvention. Result<T, E> nutzt T für Erfolg, E für Error. Iterator::Item ist ein assoziierter Typ mit beschreibendem Namen. Konsistenz mit diesen Stdlib-Patterns macht eigenen Code für Konsumenten schneller verständlich.

Generische Structs

Structs können ebenfalls Typ-Parameter haben.

Rust Generischer Struct
struct Container<T> {
    wert: T,
    beschreibung: String,
}

fn main() {
    let c1 = Container { wert: 42, beschreibung: "Zahl".into() };
    let c2 = Container { wert: "Hallo".to_string(), beschreibung: "Text".into() };
    let _ = (c1.wert, c2.wert);
}

Die Syntax struct Name<T> führt einen Parameter T ein, der in den Feldern verwendet werden kann. Der Container kann jeden Typ als wert halten — i32, String, eigene Typen, andere Generic-Container.

Beim Konstruieren wird T aus dem übergebenen Wert inferiert. Container { wert: 42, ... } macht aus dem 42 (das ein i32 ist) den Typ Container<i32>. Du musst den Typ nicht explizit angeben — außer bei mehrdeutigen Fällen, wo der Compiler ihn nicht eindeutig bestimmen kann.

Mehrere Parameter im Struct

Rust Pair-Struct
struct Pair<A, B> {
    first: A,
    second: B,
}

fn main() {
    let p = Pair { first: 42, second: "Hallo" };
    // Typ ist Pair<i32, &str>
    println!("{} {}", p.first, p.second);
}

Wie bei Funktionen können Structs mehrere Parameter haben. Beim Konstruieren werden alle aus den Feld-Werten inferiert.

Verschachtelte Generics

Rust Vec von Pair
# struct Pair<A, B> { first: A, second: B }
fn main() {
    let pairs: Vec<Pair<i32, &str>> = vec![
        Pair { first: 1, second: "eins" },
        Pair { first: 2, second: "zwei" },
    ];
    for p in &pairs {
        println!("{}: {}", p.first, p.second);
    }
}

Typ-Parameter können verschachtelt werden: Vec<Pair<i32, &str>> ist ein Vec von Pairs aus i32 und &str. Das ist nicht magisch — jeder Typ kann als Argument für einen Typ-Parameter dienen, auch andere generische Typen.

Generische Enums

Enums folgen dem gleichen Muster.

Rust Generisches Enum
enum Tree<T> {
    Leaf,
    Node(T, Box<Tree<T>>, Box<Tree<T>>),
}

fn main() {
    let baum = Tree::Node(
        1,
        Box::new(Tree::Node(2, Box::new(Tree::Leaf), Box::new(Tree::Leaf))),
        Box::new(Tree::Leaf),
    );
    let _ = baum;
}

Die Stdlib nutzt das in Option<T> und Result<T, E> — beide sind genau diese Form: ein Enum mit Typ-Parametern, die in den Varianten erscheinen. Eigene Typen folgen dem gleichen Muster.

Das Tree-Beispiel zeigt zusätzlich, wie rekursive Strukturen funktionieren: die Node-Variante referenziert sich selbst über Box<Tree<T>>. Der Box-Wrapper ist nötig, weil eine direkte Selbst-Referenz Compile-Fehler wäre (die Größe wäre rekursiv unbestimmt).

Generische impl-Blöcke

Methoden auf generischen Typen brauchen ebenfalls Typ-Parameter-Deklaration.

Rust Generisches impl
struct Container<T> {
    wert: T,
}

impl<T> Container<T> {
    fn neu(wert: T) -> Self {
        Container { wert }
    }

    fn in_wert(self) -> T {
        self.wert
    }
}

fn main() {
    let c = Container::neu(42);
    assert_eq!(c.in_wert(), 42);
}

Die Syntax impl<T> Container<T> ist auf den ersten Blick redundant — der Parameter T erscheint zweimal. In Wirklichkeit hat das eine wichtige Funktion:

impl<T> führt T als neuen Typ-Parameter im impl-Block ein.

Container<T> sagt, welchen konkreten Typ wir implementieren — hier den generischen Container mit Parameter T.

Diese Trennung erlaubt es, bedingte Implementierungen zu schreiben — siehe nächster Abschnitt.

Bedingte impl — nur für bestimmte Typen

Rust Spezifisches impl
struct Container<T> { wert: T }

// Methode auf ALLEN Containern
impl<T> Container<T> {
    fn new(wert: T) -> Self { Container { wert } }
}

// Methode nur auf Containern mit Clone
impl<T: Clone> Container<T> {
    fn duplikat(&self) -> Self {
        Container { wert: self.wert.clone() }
    }
}

// Methode nur auf Container<i32>
impl Container<i32> {
    fn doppelt(self) -> Self {
        Container { wert: self.wert * 2 }
    }
}

Mehrere impl-Blöcke können denselben Typ implementieren, jeweils mit unterschiedlichen Constraints. Der erste implementiert für alle T, der zweite nur für klonbare T, der dritte nur für genau T = i32. Dieses Pattern erlaubt sehr feingranulare API-Designs.

Type-Inference

Rust kann in den allermeisten Fällen den Typ-Parameter automatisch ermitteln.

Rust Inference
fn neu<T>(x: T) -> Vec<T> {
    vec![x]
}

fn main() {
    let v1 = neu(42);              // Vec<i32> — inferiert aus Argument
    let v2 = neu("hi");             // Vec<&str>
    let v3: Vec<f64> = neu(3.14);   // Vec<f64> — inferiert (oder explizit)
}

Der Compiler schaut sich die Argumente und die Verwendung des Rückgabewerts an, um die Typ-Parameter zu bestimmen. In den meisten Fällen reicht das.

Wann Inference scheitert

Rust Mehrdeutig
fn main() {
    // let v: Vec<_> = (1..=5).collect();   // OK — Compiler weiß Item = i32
    // let v = (1..=5).collect();           // Fehler — Compiler weiß nicht, in welchen Container

    // Lösungen:
    let v: Vec<i32> = (1..=5).collect();          // Type-Annotation
    let v = (1..=5).collect::<Vec<i32>>();         // Turbofish
}

collect() ist generisch über den Ziel-Container — Vec<T>, HashSet<T>, BTreeMap<K, V>, alles ist möglich. Ohne Context kann der Compiler nicht entscheiden, also musst du explizit werden. Zwei Wege: Type-Annotation auf der linken Seite (let v: Vec<i32>) oder Turbofish (collect::<Vec<i32>>()).

Der Turbofish ::<T> ist die explizite Form, die direkt an der Methode angegeben wird. Mehr Details im eigenen Artikel.

Default-Type-Parameter

Generic-Typ-Parameter können einen Default-Wert haben — falls beim Aufruf nichts angegeben wird.

Rust Default-Parameter
struct Buffer<T = u8> {
    daten: Vec<T>,
}

fn main() {
    let b1: Buffer = Buffer { daten: vec![1, 2, 3] };       // T = u8 (default)
    let b2: Buffer<i64> = Buffer { daten: vec![1, 2, 3] };   // T = i64 (explizit)
    let _ = (b1.daten, b2.daten);
}

Buffer<T = u8> definiert u8 als Default. Wenn der Aufrufer den Typ nicht explizit angibt, wird u8 verwendet. Sehr nützlich bei Containern, die meist mit einem bestimmten Element-Typ verwendet werden, aber gelegentlich auch mit anderen.

Bekanntestes Stdlib-Beispiel: HashMap<K, V, S = RandomState> — der Hasher-Typ S hat einen Default, sodass HashMap<String, i32> ohne explizit angegebenen Hasher funktioniert.

Default-Parameter sind ein eher fortgeschrittenes Feature. In Library-APIs sind sie wertvoll; in normalem Application-Code brauchst du sie selten.

Praxis: Typ-Parameter im echten Code

Generischer Container

Rust Stack
pub struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    pub fn neu() -> Self {
        Stack { items: Vec::new() }
    }

    pub fn push(&mut self, item: T) {
        self.items.push(item);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    pub fn len(&self) -> usize {
        self.items.len()
    }
}

fn main() {
    let mut s: Stack<i32> = Stack::neu();
    s.push(1);
    s.push(2);
    assert_eq!(s.pop(), Some(2));
    assert_eq!(s.len(), 1);
}

Ein generischer Stack — die Standard-Datenstruktur, die jeder Programmierer schon mal geschrieben hat. Mit <T> ist sie für jeden Element-Typ wiederverwendbar.

Function-Wrapper mit Closure

Rust Wrap-Funktion
fn anwenden_und_zurueck<T, F>(x: T, f: F) -> T
where F: FnOnce(&T) -> T
{
    let neu = f(&x);
    println!("Transformation: {} → {}", "x", "neu");
    neu
}

fn main() {
    let result = anwenden_und_zurueck(5, |n| n * 2);
    assert_eq!(result, 10);
}

Generic mit Closure-Typ-Parameter. F ist die Funktion/Closure, T der Wert. Solche Wrapper-Funktionen sind in funktionalen Pipeline-APIs sehr verbreitet.

Map-Datenstruktur

Rust Einfache Map
pub struct SimpleMap<K, V> {
    entries: Vec<(K, V)>,
}

impl<K: PartialEq, V> SimpleMap<K, V> {
    pub fn neu() -> Self {
        SimpleMap { entries: Vec::new() }
    }

    pub fn insert(&mut self, key: K, wert: V) {
        self.entries.push((key, wert));
    }

    pub fn get(&self, key: &K) -> Option<&V> {
        self.entries.iter()
            .find(|(k, _)| k == key)
            .map(|(_, v)| v)
    }
}

Eine vereinfachte Map mit zwei Type-Parametern. K: PartialEq erlaubt den Vergleich im find. Der Vec-basierte Storage ist O(n) für Lookups — für echte Maps nimmt man HashMap, aber als Lehrbeispiel zeigt es das generische Pattern.

Optionale Konfiguration

Rust Optional-Config
pub struct Server<Config = DefaultConfig> {
    config: Config,
}

pub struct DefaultConfig;
pub struct ProductionConfig { pub workers: u32 }

impl<C> Server<C> {
    pub fn neu(config: C) -> Self {
        Server { config }
    }
}

fn main() {
    let s1 = Server::neu(DefaultConfig);
    let s2 = Server::neu(ProductionConfig { workers: 8 });
    let _ = (s1, s2);
}

Default-Type-Parameter für API-Bequemlichkeit. Konsumenten können Server<DefaultConfig> einfach als Server schreiben, bei abweichenden Konfigurationen explizit den Typ angeben.

Generic für Iterator-Pipeline

Rust Iterator-Helper
fn first_three<I: Iterator>(mut iter: I) -> Vec<I::Item> {
    let mut result = Vec::new();
    for _ in 0..3 {
        if let Some(item) = iter.next() {
            result.push(item);
        } else {
            break;
        }
    }
    result
}

fn main() {
    let nums = vec![1, 2, 3, 4, 5];
    let first = first_three(nums.into_iter());
    assert_eq!(first, vec![1, 2, 3]);
}

I::Item greift auf den assoziierten Typ des Iterator-Traits zu — eine Art „Sub-Typ-Parameter" innerhalb des Traits. Damit kann die Funktion über beliebige Iteratoren generisch sein, und der Element-Typ wird automatisch aus dem konkreten Iterator abgeleitet.

Builder mit generischem State

Rust Stateful Builder
pub struct RequestBuilder<Body = ()> {
    url: String,
    body: Body,
}

impl RequestBuilder<()> {
    pub fn neu(url: impl Into<String>) -> Self {
        RequestBuilder { url: url.into(), body: () }
    }
}

impl<B> RequestBuilder<B> {
    pub fn with_body<NewBody>(self, body: NewBody) -> RequestBuilder<NewBody> {
        RequestBuilder { url: self.url, body }
    }
}

fn main() {
    let req = RequestBuilder::neu("/api/users").with_body("{\"id\": 1}");
    let _ = req.url;
}

Type-State-Pattern mit Generics: der Body-Typ wandert von () (kein Body) zu einem konkreten Typ, sobald with_body aufgerufen wird. Damit unterscheidet das Typ-System Builder ohne Body von solchen mit Body — Methoden, die nur mit Body Sinn machen, können auf RequestBuilder<()> nicht aufgerufen werden.

Zwei-Typen-Funktion

Rust A nach B
fn convert<A, B>(value: A) -> B
where B: From<A>
{
    value.into()
}

fn main() {
    let s: String = convert("hello");
    let n: i64 = convert(42_i32);
    let _ = (s, n);
}

Generic über zwei Typen mit Trait-Bound. Funktioniert, wann immer eine From-Konvertierung existiert. Die Stdlib-Methode Into::into() macht das aus, der Bound B: From<A> sichert die Verfügbarkeit.

Generic Newtype

Rust Newtype-Wrapper
pub struct Tagged<T> {
    pub value: T,
    pub tag: String,
}

impl<T> Tagged<T> {
    pub fn neu(value: T, tag: impl Into<String>) -> Self {
        Tagged { value, tag: tag.into() }
    }
}

fn main() {
    let t1 = Tagged::neu(42, "Antwort");
    let t2 = Tagged::neu(vec![1, 2, 3], "Liste");
    let _ = (t1.value, t2.value);
}

Newtype-artiger Wrapper, der jeden Wert mit einem Tag versieht. Generisch über den Wert-Typ — funktioniert mit Zahlen, Strings, Containern, eigenen Typen.

Interessantes

Typ-Parameter ohne Bounds — sehr wenig erlaubt.

Ein Parameter <T> ohne Constraints darf nur „herumgereicht" werden — kein Vergleich, kein Klonen, keine Methoden-Aufrufe. Sobald du etwas mit T tun willst, brauchst du einen Trait-Bound.

Konventionen: T, K/V, T/E.

Ein-Buchstaben-Namen sind Standard. T für allgemeine Typen, K/V für Map-Schlüssel/Werte, T/E für Erfolg/Error in Result-artigen Strukturen. Diese Konventionen kommen aus der Stdlib.

impl Container — zweimal T ist Absicht.

Das erste <T> führt den Parameter ein, das zweite verwendet ihn als Argument. Diese Trennung erlaubt mehrere impl-Blöcke mit unterschiedlichen Constraints, etwa impl<T: Clone> Container<T> neben impl<T> Container<T>.

Type-Inference funktioniert meist out-of-the-box.

Der Compiler infert Typ-Parameter aus Argumenten und Verwendung. Wenn er nicht eindeutig bestimmen kann (klassisch bei collect()), brauchst du Turbofish oder Type-Annotation.

Default-Type-Parameter mit T = Wert.

Bei Struct- und Trait-Definitionen kannst du Defaults setzen. Konsumenten können dann den Parameter weglassen. Stdlib-Beispiel: HashMap<K, V, S = RandomState>.

Mehrere Parameter sind unabhängig.

Bei <A, B> gibt es keine Beziehung zwischen den beiden — sie können gleich oder verschieden sein. Constraints wie where A = B gibt es nicht; das wäre eine eigene Typ-Beziehung, die anders ausgedrückt wird.

Generische Methoden funktionieren auch in nicht-generischen Structs.

Du kannst eine Methode mit eigenem <T>-Parameter in einem nicht-generischen Struct definieren. Das ist gelegentlich nützlich, etwa wenn eine einzelne Methode generisch sein soll, der Container aber nicht.

Lifetime-Parameter sind ebenfalls Generics.

&'a T ist syntaktisch ähnlich zu <T>, aber 'a ist ein Lifetime-Parameter, kein Typ-Parameter. Die beiden gehören in dieselbe Familie der Generic-Konstrukte. Mehr im Lifetimes-Kapitel.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Generics

Zur Übersicht