Einführung in Generics in Rust
Generische Programmierung (Generics) ist eines der Schlüsselkonzepte von Rust, das es ermöglicht, flexiblen, wiederverwendbaren und sicheren Code zu schreiben. Wenn Sie von anderen Sprachen (z. B. C++ oder Java) zu Rust wechseln, sind Sie wahrscheinlich bereits auf Templates oder Generics gestoßen. In Rust funktionieren Generics ähnlich, jedoch mit besonderem Fokus auf Leistung und Speichersicherheit.
Wozu braucht man Generics? Stellen Sie sich vor, Sie müssen eine Funktion schreiben, die das maximale Element in einer Liste findet. Ohne Generics müssten Sie separate Versionen für i32, f64, String und jeden benutzerdefinierten Typ schreiben. Mit Generics schreiben Sie eine Funktion, die mit jedem Typ funktioniert, der Vergleiche unterstützt.
In diesem Artikel werden wir behandeln, was Generics in Rust sind, wie man sie mit Funktionen, Strukturen und Traits verwendet, und praktische Beispiele betrachten.
Grundlagen von Generics: Funktionen und Strukturen
Generische Funktionen
Der einfachste Weg, Generics kennenzulernen, ist das Schreiben einer generischen Funktion. Die Syntax verwendet spitze Klammern <T>, wobei T ein Typparameter (type parameter) ist.
fn largest<T>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!("Größte Zahl: {}", result);
let chars = vec!['y', 'm', 'a', 'q']; let result = largest(&chars); println!("Größtes Zeichen: {}", result);}Dieser Code wird jedoch nicht kompilieren! Rust erfordert, dass der Typ T das Trait PartialOrd (für Vergleiche) implementiert. Einschränkungen (trait bounds) werden wir später behandeln, aber merken Sie sich vorerst: Generics ermöglichen die Abstraktion von konkreten Typen, aber der Compiler muss wissen, welche Operationen erlaubt sind.
Generische Strukturen
Generics funktionieren nicht nur mit Funktionen, sondern auch mit benutzerdefinierten Datentypen. Zum Beispiel kann ein Punkt mit Koordinaten ganze Zahlen oder Gleitkommazahlen speichern:
struct Point<T> { x: T, y: T,}
fn main() { let integer_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.0, y: 4.0 }; println!("Ganzzahl-Punkt: ({}, {})", integer_point.x, integer_point.y); println!("Gleitkomma-Punkt: ({}, {})", float_point.x, float_point.y);}Man kann mehrere Typparameter verwenden, wenn die Felder unterschiedliche Typen haben:
struct Pair<T, U> { first: T, second: U,}
fn main() { let pair = Pair { first: 42, second: "Antwort" }; println!("Paar: {} und {}", pair.first, pair.second);}Generics mit Traits und Einschränkungen (Trait Bounds)
Was sind trait bounds?
Trait bounds sind eine Möglichkeit, dem Compiler mitzuteilen, dass ein generischer Typ bestimmte Traits implementieren muss. Ohne sie können wir keine Methoden auf einem generischen Wert aufrufen. Kehren wir zur Funktion largest zurück und korrigieren sie:
use std::cmp::PartialOrd;
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; println!("Größtes: {}", largest(&numbers));}Jetzt akzeptiert die Funktion nur Typen, die Vergleiche unterstützen (PartialOrd).
Mehrere Einschränkungen
Sie können mehrere Traits mit + angeben. Zum Beispiel muss ein Typ gleichzeitig vergleichbar und klonbar sein:
use std::fmt::Display;
fn print_and_compare