Generics in Rust: Warum sie wichtig sind und wie man sie verwendet
Generics (Verallgemeinerungen) sind eines der Schlüsselkonzepte in Rust, die es ermöglichen, flexiblen, wiederverwendbaren und sicheren Code zu schreiben. Wenn Sie gerade erst anfangen, Rust zu lernen, eröffnet das Verständnis von Generics den Weg zum Schreiben effizienter Bibliotheken und Anwendungen. In diesem Artikel werden wir untersuchen, was Generics sind, warum sie benötigt werden und wie man sie in der Praxis anwendet.
Was sind Generics und warum werden sie benötigt?
Stellen Sie sich vor, Sie müssen eine Funktion schreiben, die das maximale Element in einem Array findet. Für Zahlen gibt es eine Implementierung, für Zeichenketten eine andere. Um den Code nicht für jeden Typ zu kopieren, bietet Rust generische Programmierung mit Generics an.
Generics ermöglichen es, Strukturen, Funktionen, Aufzählungen oder Methoden zu definieren, die mit einem beliebigen Datentyp arbeiten, ohne an einen bestimmten gebunden zu sein. Der Compiler setzt während der Kompilierung den tatsächlichen Typ ein – dieser Prozess wird Monomorphisierung genannt.
Ohne Generics würden Sie schreiben:
fn max_i32(list: &[i32]) -> i32 { let mut max = list[0]; for &item in list { if item > max { max = item; } } max}
fn max_f64(list: &[f64]) -> f64 { let mut max = list[0]; for &item in list { if item > max { max = item; } } max}Mit Generics wird daraus eine einzige universelle Funktion.
Grundlagen der Verwendung von Generics
Generic-Funktionen
Um eine Funktion generisch zu machen, müssen Sie einen Typparameter in spitzen Klammern <T> angeben:
fn max<T>(list: &[T]) -> T { let mut max = &list[0]; for item in list { if item > max { max = item; } } *max}Dieser Code wird jedoch nicht kompiliert – der Compiler wird sagen, dass für den Typ T das Trait PartialOrd nicht implementiert ist. Hier kommen Trait-Bounds ins Spiel.
Generic-Strukturen und Aufzählungen
Strukturen können ebenfalls generisch sein:
struct Point<T> { x: T, y: T,}
fn main() { let int_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.5, y: 3.8 };}Sie können mehrere Typparameter angeben:
struct Pair<T, U> { first: T, second: U,}Aufzählungen unterstützen ebenfalls Generics – denken Sie an Option<T> oder Result<T, E> aus der Standardbibliothek.
Generic-Methoden
Für Strukturen mit Generics können Methoden definiert werden:
impl<T> Point<T> { fn x(&self) -> &T { &self.x }}Oder nur für einen bestimmten Typ:
impl Point<f64> { fn distance_from_origin(&self) -> f64 { (self.x.powi(2) + self.y.powi(2)).sqrt() }}Trait-Bounds
Um Vergleichs-, Additions- oder andere Operationen zu verwenden, muss angegeben werden, welche Traits der Typ implementiert. Kehren wir zur Funktion max zurück:
fn max<T: PartialOrd>(list: &[T]) -> T { let mut max = &list[0]; for item in list { if item > max { max = item; } } *max}Jetzt kompiliert der Code – wir haben gesagt, dass T partielle Ordnung (PartialOrd) unterstützen muss.
Traits können mit + kombiniert werden:
fn clone_and_compare<T: Clone + PartialOrd>(a: T, b: T) -> bool { a.clone() > b.clone()}Oder die where-Syntax für bessere Lesbarkeit verwenden:
fn some_function<T, U>(t: &T, u: &U) -> i32where T: