Modul 8 — Traits & Generics
Generics: Typen als Parameter
Eine Funktion für jede Typvariante? largest_i32, largest_char,
largest_string — dreimal derselbe Code. Generics machen den Typ zum
Parameter. Aber Achtung: ohne Zusagen kompiliert nicht einmal ein Vergleich:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list {
if item > max {
max = item;
}
}
max
}
fn main() {
let zahlen = vec![3, 9, 4];
let zeichen = vec!['y', 'a', 'm'];
println!("{}", largest(&zahlen));
println!("{}", largest(&zeichen));
}
Das : PartialOrd ist ein Trait-Bound — eine Zusage an den Compiler:
"jedes T, mit dem du diese Funktion instanziierst, kann verglichen werden".
Ohne den Bound würde die Zeile item > max den Compiler stoppen: er kennt
die Fähigkeiten von T nicht.
Auch Structs können generisch sein — mit so vielen Typparametern wie Felder brauchen:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let ganzzahlig = Point { x: 3, y: -2 };
let gemischt = Point { x: 1, y: 0.5 };
println!("{:?} und {:?}", ganzzahlig, gemischt);
}
Der Typ von gemischt wird inferiert — die Parameter T und U füllen
sich automatisch. Und das Beste: Generics kosten nichts zur Laufzeit.
Der Compiler erzeugt pro verwendetem konkreten Typ eine spezialisierte
Kopie (Monomorphisierung) — derselbe Maschinencode, als hättest du
jede Funktion von Hand geschrieben.
📝 Schnellprüfung
Was leistet Monomorphisierung?
Übung: der Bound fehlt
🧩 Übung m8-l1-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Traits: gemeinsames Verhalten
Generics sagen "irgendein Typ". Traits sagen "irgendein Typ, der DIES kann" — und definieren, was DIES bedeutet. Ein Trait ist ein Vertrag aus Methodensignaturen:
trait Summarize {
fn author(&self) -> &str;
fn title(&self) -> &str;
fn summary(&self) -> String {
format!("{} von {}", self.title(), self.author())
}
}
struct Article {
title: String,
author: String,
}
impl Summarize for Article {
fn author(&self) -> &str {
&self.author
}
fn title(&self) -> &str {
&self.title
}
}
fn notify(item: &impl Summarize) {
println!("Neu: {}", item.summary());
}
fn main() {
let a = Article {
title: String::from("Rust im Browser"),
author: String::from("Tobias"),
};
notify(&a);
}
Drei Konzepte in einem Beispiel: summary hat einen Rumpf — eine
Default-Implementierung, die Article erbt, ohne sie zu schreiben.
impl Summarize for Article ist die Implementierung des Vertrags.
Und &impl Summarize in notify akzeptiert jeden Typ, der den Vertrag
erfüllt — Article heute, ein Tweet morgen.
Trait-Bounds verbinden Generics mit Verhalten
fn notify(item: &impl Summarize)— kurzform für einen generischen Parameter- Bounds dokumentieren: was die Funktion wirklich vom Typ braucht
- Default-Methoden machen Traits erweiterbar, ohne alle Implementierer zu brechen
📝 Schnellprüfung
Article implementiert nur author und title. Warum darf notify item.summary() aufrufen?
Übung: der fehlende Vertrag
🧩 Übung m8-l2-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Die Std-Traits, die jeder kennt
Vieles vom Standardverhalten kommt aus Standard-Traits — meist per derive, manchmal von Hand:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Money {
euros: u32,
cents: u8,
}
fn main() {
let preis = Money { euros: 12, cents: 34 };
let rabatt = Money { euros: 9, cents: 99 };
println!("{preis:?}");
println!("teurer: {}", preis > rabatt);
}
Was jeder einzelne bringt:
| Trait | gibt | per derive? |
|---|---|---|
| Debug | {:?}-Ausgabe | ja |
| Clone / Copy | duplizieren / kopieren beim Zuweisen | ja |
| PartialEq / Eq | == und != | ja |
| PartialOrd / Ord | <, >, sortieren | ja |
| Default | ::default() mit Nullwerten | ja |
| Hash | als HashMap-Schlüssel | ja |
| Display | {}-Ausgabe für Nutzer | von Hand |
Display zeigt, warum "von Hand": Nutzerausgabe will FORMAT — Währung, Dezimalstellen, Locale. Das kann kein derive erraten:
use std::fmt;
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{},{:02} €", self.euros, self.cents)
}
}
fn main() {
let m = Money { euros: 12, cents: 4 };
println!("{m}");
}
Und From/Into — das Modul-7-Brücke-Werkzeug: implementierst du From, bekommst du Into gratis dazu.
📝 Schnellprüfung
Warum kannst du Copy für Money ableiten, aber nicht für einen Struct mit String-Feld?
Übung: Money komplett machen
🧩 Übung m8-l3-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Mini-Projekt: stats-lib
Zeit für ein Mini-Projekt mit Produktqualität: eine kleine Statistik- Bibliothek. Der Clou ist die API-Entscheidung: Mittelwert und Median geben ein Option zurück — denn eine leere Datenreihe hat keinen Mittelwert.
🏢 Praxis-Szenario: Die Auswertung, die lügt
Dein Team analysiert Messreihen. Ein Kollege schrieb eine mean-Funktion, die bei leerer Eingabe 0.0 zurückgibt. Folge: Dashboards zeigen plötzlich 'Durchschnitt 0 ms' — und niemand merkt, dass die Datenquelle leer war. Der Fehler saß nicht in der Berechnung, sondern in der API: sie hatte keinen Weg zu sagen 'kein Ergebnis'.
🧩 Übung m8-l4-e1
🔮 Vorhersage: Was passiert beim Ausführen?
Was du in Modul 8 geschafft hast
- Generics mit Bounds: eine Funktion statt vier Kopien — gratis durch Monomorphisierung
- Eigene Traits mit Default-Methoden; &impl Trait als offener Parameter
- Die Std-Traits: Debug/Clone/Copy/Vergleich per derive, Display/From von Hand
- Eine API, die 'kein Ergebnis' als Wert ausdrückt — statt zu lügen
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Modul-Checkpoint
Checkpoint: Modul 8
Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.
Bereit, dein Wissen zu testen?
Probiere unsere Übungsprüfungen mit Hunderten von realistischen Fragen aus.
Üben starten →