Introduction : que sont les Generics et pourquoi en a-t-on besoin en Rust ?
\nImaginez que vous deviez écrire une fonction qui trouve le plus grand élément dans une liste. Pour les entiers — une fonction, pour les nombres à virgule flottante — une autre, pour les chaînes de caractères — une troisième. Le code serait dupliqué et deviendrait difficile à maintenir. En Rust, ce problème est résolu par les Generics (programmation générique).
\nLes Generics permettent d'écrire du code qui fonctionne avec différents types de données, sans perdre pour autant le typage strict. Au lieu de dupliquer les fonctions pour chaque type, vous écrivez une version générique unique. Lors de la compilation, Rust substitue le type concret et vérifie la correction. Cela offre :
\n- \n
- Réutilisabilité du code — une seule fonction pour tous les types. \n
- Sécurité — toutes les erreurs de type sont détectées lors de la compilation. \n
- Performance — grâce à la monomorphisation (le compilateur génère un code séparé pour chaque type utilisé), le code générique est aussi rapide que le code spécialisé. \n
Dans cet article, nous allons voir comment utiliser les Generics en Rust avec des exemples pratiques, en commençant par des fonctions simples et en terminant par des structures et des traits.
\n\n1. Generics dans les fonctions : premier pas vers l'universalité
\nSyntaxe et exemple de base
\nPour rendre une fonction générique, il faut spécifier un paramètre de type entre chevrons <T>. On utilise généralement des lettres majuscules : T, U, V. Voici à quoi ressemble une fonction qui retourne la valeur passée :
fn identity<T>(value: T) -> T {\n value\n}\n\nfn main() {\n let num = identity(42);\n let text = identity("Hello");\n println!("{} {}", num, text); // Affiche : 42 Hello\n}\nLe compilateur déduit lui-même le type en fonction de l'argument passé. Vous pouvez aussi spécifier le type explicitement : identity::<i32>(42).
Contraintes : les traits comme limites de types
\nQue faire si nous voulons comparer des valeurs ? Écrire simplement value1 > value2 ne fonctionnera pas — tous les types ne supportent pas la comparaison. Il faut spécifier que le type doit implémenter le trait PartialOrd :
fn largest<T: PartialOrd>(list: &[T]) -> &T {\n let mut largest = &list[0];\n for item in list {\n if item > largest {\n largest = item;\n }\n }\n largest\n}\n\nfn main() {\n let numbers = vec![34, 50, 25, 100, 65];\n println!("Le plus grand nombre : {}", largest(&numbers));\n\n let chars = vec!['y', 'm', 'a', 'q'];\n println!("Le plus grand caractère : {}", largest(&chars));\n}\nIci, T: PartialOrd est une contrainte de trait. Elle garantit que l'opérateur de comparaison est défini pour le type T. Sans cette contrainte, le code ne se compilerait pas.
2. Generics dans les structures et les énumérations
\nStructures avec champs génériques
\nLes Generics fonctionnent également très bien avec les types personnalisés. Par exemple, la structure Point peut stocker des coordonnées de n'importe quel type :
struct Point<T> {\n x: T,\n y: T,\n}\n\nfn main() {\n let integer_point = Point { x: 5, y: 10 };\n let float_point = Point { x: 1.0, y: 4.0 };\n // let mixed = Point { x: 5, y: 4.0 }; // Erreur ! Les deux champs doivent être du même type\n}\nSi vous avez besoin que les champs puissent être de types différents, utilisez plusieurs paramètres :
\nstruct Point<T, U> {\n x: T,\n y: U,\n}\n\nfn main() {\n let mixed = Point { x: 5, y: 4.0 }; // Maintenant ça fonctionne\n}\n\nÉnumérations avec Generics
\nL'énumération standard Option<T> est un excellent exemple d'énumération générique. Vous pouvez créer la vôtre :
enum Result<T, E> {\n Ok(T),\n Err(E),\n}\n\n// Utilisation\nfn divide(a: f64, b: f64) -> Result<f64, String> {\n if b