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.
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
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.
| Buchstabe | Verwendung |
|---|---|
T | Generischer Typ (Standard) |
U, V, ... | Weitere Typ-Parameter neben T |
K | Key-Typ (HashMap, BTreeMap) |
V | Value-Typ (HashMap, BTreeMap) |
E | Error-Typ (Result) |
A, B, C | Bei Tupeln oder funktionalen APIs |
R | Return-Typ (selten, bei Closures) |
F, G | Function/Closure-Typ |
I | Iterator-Typ |
S | Self/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.
// 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.
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
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
# 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.
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.
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
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.
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
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.
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
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
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
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
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
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
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
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
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
- The Rust Book – Generic Data Types
- Rust Reference – Generic Parameters
- Rust by Example – Generics
- Rust API Guidelines – Naming