Generics in Rust: Warum du sie unbedingt brauchst
\n\nWenn du anfängst, mit Rust zu programmieren, fühlt sich das Typsystem oft wie ein Feind an. Der Compiler meckert über jedes Komma und das Wort Generic lässt dich erzittern. Aber in Wahrheit sind Generics (generische Typen) dein bester Freund. Sie ermöglichen dir, flexiblen, sicheren und wiederverwendbaren Code zu schreiben – ohne Performance-Einbußen.
In diesem Artikel tauchen wir tief in die Welt der Generics in Rust ein. Du lernst, wie du sie in Funktionen, Strukturen und Traits einsetzt. Du wirst sehen: Generische Programmierung ist keine Magie, sondern ein mächtiges Werkzeug, das deinen Code sauberer und schneller macht.
\n\nWas sind Generics und warum braucht man sie?
\n\nStell dir vor, du schreibst eine Funktion, die das größte Element in einem Array findet. Ohne Generics müsstest du für jeden Datentyp (i32, f64, String usw.) eine separate Implementierung schreiben. Das ist zeitaufwendig, mühsam und verletzt das DRY-Prinzip (Don't Repeat Yourself).
Generics lösen dieses Problem. Sie erlauben es, Typen zu parametrisieren: Du schreibst eine einzige Funktion, die mit jedem Datentyp funktioniert. Der Compiler generiert dann automatisch die passenden Code-Versionen für jeden konkreten Fall. Dieser Vorgang heißt Monomorphisierung.
\n\nSyntax der Generics: Vom Einfachen zum Komplexen
\n\n1. Generische Funktionen
\n\nBeginnen wir mit einem einfachen Beispiel. Angenommen, wir wollen eine Funktion, die den übergebenen Wert einfach zurückgibt (Identitätsfunktion):
\n\nfn identity<T>(value: T) -> T {\n value\n}\n\nfn main() {\n let n = identity(42);\n let s = identity("hallo");\n println!("{}, {}", n, s); // 42, hallo\n}\n\nHier ist <T> der Typparameter. T kann jeder beliebige Typ sein: Zahl, String, Struktur. Beim Aufruf errät der Compiler selbstständig, welcher Typ gemeint ist. Das funktioniert nicht durch dynamische Dispatch (wie in Java oder Python), sondern durch die Generierung von spezialisiertem Code während der Kompilierung. Deshalb opfert Rust keine Performance.
2. Typeinschränkungen (Trait Bounds)
\n\nWas aber, wenn wir nicht nur einen Wert übergeben, sondern zum Beispiel zwei Zahlen vergleichen wollen? Ein einfaches <T> reicht nicht, weil nicht für alle Typen ein Vergleichsoperator definiert ist. Wir müssen angeben, dass T das Trait PartialOrd implementieren muss:
fn max<T: PartialOrd>(a: T, b: T) -> T {\n if a > b { a } else { b }\n}\n\nfn main() {\n println!("{}", max(10, 20)); // 20\n println!("{}", max(3.14, 2.71)); // 3.14\n}\n\nDie Schreibweise T: PartialOrd ist ein Trait Bound. Sie sagt dem Compiler: „Verwende nur Typen, für die eine Vergleichsordnung definiert ist.“ Versuche, zwei Strings an max zu übergeben – es funktioniert, weil String ebenfalls PartialOrd implementiert. Übergibst du jedoch eine Struktur ohne diese Implementierung, erhältst du einen Kompilierungsfehler.
3. Mehrfacheinschränkungen
\n\nManchmal muss ein Typ gleich mehrere Traits erfüllen. Zum Beispiel soll er sowohl vergleichbar als auch klonbar sein:
\n\nfn clone_and_max<T: PartialOrd + Clone>(a: T, b: T) -> T {\n let a_cloned = a.clone();\n let b_cloned = b.clone();\n if a_cloned > b_cloned { a } else { b }\n}\n\nDie Syntax T: PartialOrd + Clone bedeutet, dass der Typ beide Traits implementieren muss. Das ist mächtig, aber übertreibe es nicht: Zu viele Einschränkungen verschlechtern die Lesbarkeit.
Generics in Strukturen und Aufzählungen
\n\nGenerische Strukturen
\n\nGenerics funktionieren nicht nur mit Funktionen, sondern auch mit benutzerdefinierten Typen. Ein klassisches Beispiel ist die Struktur Point, die Koordinaten beliebigen Typs speichern kann:
struct Point<T> {\n x: T,\n y: T,\n}\n\nimpl<T> Point<T> {\n fn new(x: