Generics en Rust : pourquoi en avez-vous besoin et comment les utiliser
Les Generics (génériques) sont l'un des concepts clés de Rust, permettant d'écrire un code flexible, réutilisable et sûr. Si vous commencez tout juste à apprendre Rust, comprendre les génériques vous ouvrira la voie à l'écriture de bibliothèques et d'applications efficaces. Dans cet article, nous allons voir ce que sont les Generics, pourquoi ils sont nécessaires et comment les utiliser en pratique.
Que sont les Generics et pourquoi en avez-vous besoin ?
Imaginez que vous devez écrire une fonction qui trouve l'élément maximum dans un tableau. Pour les nombres, ce serait une implémentation, pour les chaînes, une autre. Pour éviter de copier le code pour chaque type, Rust propose la programmation générique avec Generics.
Les génériques permettent de définir des structures, fonctions, énumérations ou méthodes qui fonctionnent avec un type de données arbitraire, sans se lier à un type spécifique. Le compilateur, lors de la compilation, substitue le type réel — ce processus est appelé monomorphisation.
Sans génériques, vous écririez :
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}Avec les génériques, cela se transforme en une seule fonction universelle.
Bases de l'utilisation des Generics
Fonctions génériques
Pour rendre une fonction générique, vous devez spécifier un paramètre de type entre chevrons <T> :
fn max<T>(list: &[T]) -> T { let mut max = &list[0]; for item in list { if item > max { max = item; } } *max}Mais ce code ne compilera pas — le compilateur dira que le trait PartialOrd n'est pas implémenté pour le type T. C'est là qu'interviennent les contraintes de traits.
Structures et énumérations génériques
Les structures peuvent également être génériques :
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 };}Vous pouvez spécifier plusieurs paramètres de type :
struct Pair<T, U> { first: T, second: U,}Les énumérations supportent également les génériques — pensez à Option<T> ou Result<T, E> de la bibliothèque standard.
Méthodes génériques
Pour les structures avec génériques, vous pouvez définir des méthodes :
impl<T> Point<T> { fn x(&self) -> &T { &self.x }}Ou uniquement pour un type spécifique :
impl Point<f64> { fn distance_from_origin(&self) -> f64 { (self.x.powi(2) + self.y.powi(2)).sqrt() }}Contraintes de traits (Trait Bounds)
Pour utiliser des opérations de comparaison, d'addition ou d'autres méthodes, vous devez spécifier quels traits le type implémente. Revenons à la fonction max :
fn max<T: PartialOrd>(list: &[T]) -> T { let mut max = &list[0]; for item in list { if item > max { max = item; } } *max}Maintenant, le code compile — nous avons dit que T doit supporter le classement partiel (PartialOrd).
Vous pouvez combiner des traits avec + :
fn clone_and_compare<T: Clone + PartialOrd>(a: T, b: T) -> bool { a.clone() > b.clone()}Ou utiliser la syntaxe where pour plus de lisibilité :
fn some_function<T, U>(t: &T, u: &U) -> i32where T: