Generics sind die Antwort auf eine fundamentale Frage in der Software-Entwicklung: wie schreibe ich Code, der mit verschiedenen Typen funktioniert, ohne ihn für jeden Typ kopieren zu müssen? In Rust werden Generics über Typ-Parameter ausgedrückt — ein Platzhalter wie <T>, der bei der Verwendung durch einen konkreten Typ ersetzt wird. Das Besondere an Rusts Implementierung ist die Monomorphisierung: zur Compile-Zeit wird für jede konkrete Typ-Kombination spezialisierter Maschinencode erzeugt. Das macht Generics in Rust kostenlos zur Laufzeit — keine Box, keine Vtable, keine Indirection. Dieses Kapitel führt durch alle Aspekte: von einfachen Type-Parametern bis zu Const-Generics, mit dem Schwerpunkt auf den mentalen Modellen, die du brauchst, um Generic-Code idiomatisch und sicher zu schreiben.
Was Generics lösen
Stell dir vor, du schreibst eine Funktion, die das größte Element in einer Liste findet. Ohne Generics müsstest du eine Variante für jeden Typ schreiben:
fn groesstes_i32(zahlen: &[i32]) -> &i32 {
let mut groesstes = &zahlen[0];
for n in zahlen { if n > groesstes { groesstes = n; } }
groesstes
}
fn groesstes_f64(zahlen: &[f64]) -> &f64 {
let mut groesstes = &zahlen[0];
for n in zahlen { if n > groesstes { groesstes = n; } }
groesstes
}
// Und so weiter für jeden Typ...Die Logik ist identisch — nur die Typen unterscheiden sich. Das ist Code-Duplikation, die jede Änderung zum Albtraum macht (du musst die Korrektur in jeder Variante nachziehen).
Mit Generics schreibst du die Logik einmal und parametrisierst über den Typ:
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, 7];
let words = ["apple", "banana", "cherry"];
println!("{}", groesstes(&zahlen)); // 42
println!("{}", groesstes(&words)); // cherry
}Das <T: PartialOrd> führt einen Typ-Parameter T ein, der vergleichbar sein muss (PartialOrd-Trait). Der Compiler prüft beim Aufruf, ob die übergebenen Typen die Constraints erfüllen, und generiert hinter den Kulissen spezialisierten Code für jeden verwendeten Typ.
Wo Generics in Rust auftauchen
Typ-Parameter funktionieren an vielen Stellen in der Sprache — überall, wo du „beliebiger Typ" ausdrücken willst.
// 1. In Funktionen
fn identitaet<T>(x: T) -> T { x }
// 2. In Structs
struct Pair<A, B> { first: A, second: B }
// 3. In Enums (Option, Result aus der Stdlib)
enum MeinResultat<T, E> {
Ok(T),
Err(E),
}
// 4. In Methoden (impl-Blöcken)
impl<A, B> Pair<A, B> {
fn neu(a: A, b: B) -> Self { Pair { first: a, second: b } }
}
// 5. In Traits
trait Container<T> {
fn add(&mut self, item: T);
fn get(&self) -> Option<&T>;
}Die fünf wichtigsten Stellen. Aus der Stdlib kennst du sie alle: Vec<T>, Option<T>, Result<T, E>, HashMap<K, V>, Box<T>, &[T]. Praktisch jeder Container in der Stdlib ist generisch — das ist es, was sie überhaupt nützlich macht.
Zwei Schichten: Type-Parameter und Trait-Bounds
Generics in Rust haben zwei eng verzahnte Konzepte, die du sauber unterscheiden solltest.
Typ-Parameter (<T>) sind die Platzhalter — sie sagen „hier kommt ein Typ rein, der zur Aufruf-Zeit bestimmt wird". Ohne weitere Einschränkungen darf T jeder Typ sein. Das bedeutet aber auch: du kannst sehr wenig mit ihm anfangen. Du kannst ihn herumreichen, in Variablen binden, an andere generische Funktionen weitergeben — aber du kannst nichts spezifisches mit ihm tun (keine Vergleiche, keine Arithmetik, kein Klonen).
Trait-Bounds (<T: SomeTrait>) schränken T ein. Sie sagen „T muss diesen Trait implementieren". Damit gewinnst du Capabilities — Methoden und Operationen, die der Trait garantiert. Mit T: PartialOrd darfst du >/< verwenden, mit T: Clone kannst du .clone() aufrufen, mit T: Display ist println!("{t}") möglich.
Diese Trennung ist die zentrale Designentscheidung: Generics sind kein „akzeptiere alles, vertrau auf Duck-Typing" (wie in Python), sondern explizit constrained. Was der generische Code mit T tun kann, ist durch die Bounds beschrieben — der Compiler prüft das genau.
Was dich erwartet
- Typ-Parameter — die Syntax in Funktionen, Structs, Enums, Methoden. Type-Inference, mehrere Parameter, Default-Type-Parameter.
- Trait-Bounds — wie
T: SomeTraitfunktioniert, kombinierte Bounds mit+, die wichtigsten Stdlib-Traits (Clone,Copy,Debug,Display,PartialEq,PartialOrd,Default), und dieimpl Trait-Syntax als Kurzform. - where-Klauseln — die alternative Syntax für komplexe Bounds. Wann lieber inline, wann lieber where, und welche Lesbarkeits-Vorteile entstehen.
- Turbofish — der
::<T>-Operator zur expliziten Typ-Angabe. Wann er nötig ist (mehrdeutige Inference) und wann er weggelassen werden kann. - Monomorphisierung — die Compile-Zeit-Mechanik: wie der Compiler für jede Typ-Kombination eine spezialisierte Version erzeugt. Folgen für Performance und Binary-Größe.
- Const-Generics — Typ-Parameter, die konstante Werte statt Typen sind.
[T; N]-Arrays als prominentes Beispiel.
Was du nach diesem Kapitel kannst
- Typ-Parameter in Funktionen, Structs, Enums und Methoden idiomatisch einsetzen.
- Trait-Bounds gezielt nutzen, um genau die Capabilities zu fordern, die der Code braucht — nicht mehr, nicht weniger.
- Komplexe Bounds in
where-Klauseln strukturieren, um die Funktions-Signatur lesbar zu halten. - Den Turbofish bei Bedarf einsetzen und Inference-Stolperfallen erkennen.
- Verstehen, wie Monomorphisierung dein Code beeinflusst: Zero-Cost-Abstraktion auf der einen Seite, größere Binaries und langsamere Builds auf der anderen.
- Const-Generics für Typ-Sicherheit mit konstanten Größen einsetzen — etwa für mathematische Vektoren oder feste Buffer.
Im nächsten Kapitel geht es zu Traits — dem Polymorphismus-Mechanismus, der Generics erst richtig nützlich macht.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Generic Types, Traits, and Lifetimes
- Rust Reference – Generics
- Rust by Example – Generics
- The Rustonomicon – Generics