Les types génériques (generics) permettent d'écrire du code indépendant des types spécifiques. Ils sont implémentés à l'aide de la syntaxe des crochets angulaires :
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
Ici, T est un type générique, restreint par le trait PartialOrd.
Paramètres génériques sont déclarés via <T>, mais ils peuvent être restreints à l'aide de limites de traits via deux-points, par exemple, <T: Display>. C'est un moyen d'informer le compilateur que seuls les types pour lesquels le trait requis est implémenté peuvent être utilisés.
En Rust, on distingue deux formes de dispatch pour les génériques :
dyn Trait est utilisé, un appel se fait à travers la table virtuelle (vtable).Influence sur le code machine : L'utilisation de génériques avec des limites de traits (sans dyn Trait) conduit à la monomorphisation : augmentation de la taille binaire, mais vitesse maximale. L'utilisation de dyn Trait permet d'économiser de la taille binaire, mais entraîne une baisse de performance.
Question : Voici une fonction
fn do_something<T: Debug>(value: &T)
Le compilateur va-t-il créer une fonction distincte do_something dans le code binaire pour chaque type avec lequel elle est utilisée, ou utilisera-t-il une implémentation universelle ?
Réponse typiquement incorrecte : Il utilisera une seule fonction pour tous les types grâce à la limite de traits.
Bonne réponse : Le compilateur crée des copies distinctes de cette fonction pour chaque type (monomorphisation), puisque la limite de traits ne rend pas la fonction générique "universelle" via vtable. L'universalité n'apparaît qu'avec dyn Trait (dispatch dynamique).
Exemple :
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // Pour chaque appel avec un type différent, une version distincte de la fonction sera créée
Histoire
Dans un projet avec de gros objets génériques, il a été constaté que le fichier binaire était considérablement plus grand que prévu. Il a été découvert plus tard : la raison — l'utilisation étendue de fonctions génériques sans restrictions. Les appels avec des dizaines de types ont conduit à une croissance exponentielle de la taille du fichier exécutable (code bloat), que l'on n'a remarquée qu'à la compilation en mode release sur CI.
Histoire
Un des développeurs prenait un paramètre générique avec une limite de trait, pensant que ce code fonctionnait avec un dispatch "dynamique". Cela a conduit à un gaspillage de mémoire sur le serveur et à une diminution des performances en raison de la croissance continue du code et de son caching par le processeur.
Histoire
Dans une bibliothèque, on a essayé d'utiliser un trait générique avec un type
Self(par exemple, traitClone) en tant quedyn Trait, ce qui n'est pas pris en charge en Rust et a entraîné une erreur de compilation. Il fallait réécrire explicitement l'interface, sinon l'API générique ne fonctionnerait pas en mode dynamique, et l'interface aurait dû être modifiée au niveau du compile-time.