Le mot-clé `dyn` : Origines, Signification et Limitations
Table des matières
En Rust, le mot-clé dyn est au cœur du polymorphisme dynamique, permettant d'écrire du code flexible qui peut travailler avec différents types à l'exécution. Mais d'où vient ce mot mystérieux, comment fonctionne-t-il, et pourquoi certaines constructions comme Vec<dyn Trait> sont-elles impossibles ? Plongeons dans les mécanismes internes du langage.
Origine et Signification de dyn
Le mot dyn est l'abréviation de "dynamic" (dynamique en anglais). Il a été introduit dans l'édition 2018 de Rust pour remplacer la syntaxe des traits objets sans mot-clé explicite, rendant le code plus clair et explicite.
Évolution Historique
Avant Rust 2018, les traits objets s'écrivaient simplement avec &Trait :
// Ancienne syntaxe (avant 2018)
fn process_trait(obj: &Trait) {
obj.do_something();
}
Avec Rust 2018, dyn est devenu obligatoire :
// Nouvelle syntaxe (2018+)
fn process_trait(obj: &dyn Trait) {
obj.do_something();
}
Cette évolution a permis de distinguer clairement le polymorphisme statique (génériques) du polymorphisme dynamique (traits objets).
Pourquoi "dyn" ?
Le choix de dyn reflète la nature du dispatch dynamique (dynamic dispatch). Contrairement au polymorphisme statique où la méthode à appeler est déterminée à la compilation, avec dyn, la résolution se fait à l'exécution via une table de fonctions virtuelles (vtable).
trait Animal {
fn faire_du_bruit(&self);
}
struct Chien;
struct Chat;
impl Animal for Chien {
fn faire_du_bruit(&self) {
println!("Wouf!");
}
}
impl Animal for Chat {
fn faire_du_bruit(&self) {
println!("Miaou!");
}
}
fn faire_parler(animal: &dyn Animal) {
animal.faire_du_bruit(); // Résolution à l'exécution
}
L'Opposé de dyn : Le Polymorphisme statique
L'opposé conceptuel de dyn n'est pas un mot-clé spécifique, mais plutôt l'ensemble des mécanismes de polymorphisme statique :
Génériques avec Traits Liés
fn faire_parler_generique<T: Animal>(animal: &T) {
animal.faire_du_bruit(); // Résolution à la compilation
}
Impl Trait
fn creer_animal() -> impl Animal {
Chien {} // Type concret connu à la compilation
}
Tableau Comparatif
| Aspect | dyn Trait (Dynamique) |
Génériques (Statique) |
|---|---|---|
| Résolution | Exécution (vtable) | Compilation (monomorphisation) |
| Performance | Overhead d'indirection | Optimisations maximales |
| Flexibilité | Types multiples à l'exécution | Type unique à la compilation |
| Taille | Pointeur gras (data + vtable) | Taille du type concret |
Le problème de Vec<dyn Trait>
La limitation la plus surprenante pour les nouveaux Rustacés est l'impossibilité d'écrire Vec<dyn Trait>. Explorons les raisons techniques.
Le système de taille (Sized Trait)
En Rust, tous les types doivent avoir une taille connue à la compilation. C'est ce qu'exprime le trait Sized. Or, dyn Trait n'est pas Sized.
// Ceci ne compile PAS !
// let v: Vec<dyn Animal> = vec![];
Pourquoi dyn Trait n'est pas Sized ?
Un trait objet dyn Trait peut représenter n'importe quel type implémentant le trait, et ces types peuvent avoir des tailles différentes :
struct PetitChien; // Taille : 0 octets
struct GrosChien {
nom: String, // Taille : 24 octets
age: u32, // + 4 octets
}
Comment stocker ces types de tailles différentes dans un Vec qui nécessite des éléments de taille uniforme ?
Les solutions pratiques
1. Pointeurs Intelligents (Box, Rc, Arc)
let animaux: Vec<Box<dyn Animal>> = vec![
Box::new(Chien),
Box::new(Chat),
];
2. Références avec Lifetime
fn traiter_animaux(animaux: &[&dyn Animal]) {
for animal in animaux {
animal.faire_du_bruit();
}
}
3. Enum (Alternative statique)
enum AnimalEnum {
Chien(Chien),
Chat(Chat),
}
impl Animal for AnimalEnum {
fn faire_du_bruit(&self) {
match self {
AnimalEnum::Chien(chien) => chien.faire_du_bruit(),
AnimalEnum::Chat(chat) => chat.faire_du_bruit(),
}
}
}
Mécanisme des pointeurs Gras
Quand tu utilises Box<dyn Trait>, tu utilises un pointeur gras (fat pointer) :
let chien: Box<dyn Animal> = Box::new(Chien);
Ce pointeur contient :
- Un pointeur vers les données (Chien)
- Un pointeur vers la vtable (table des méthodes Animal pour Chien)
Implications des choix de conception
Considérations de performance
// Dispatch statique - plus rapide
fn benchmark_statique<T: Animal>(animaux: &[T]) {
for animal in animaux {
animal.faire_du_bruit(); // Appel direct
}
}
// Dispatch dynamique - plus flexible
fn benchmark_dynamique(animaux: &[&dyn Animal]) {
for animal in animaux {
animal.faire_du_bruit(); // Appel via vtable
}
}
Trade-off flexibilité vs performance
Le choix entre dyn et les génériques représente un compromis classique :
- Génériques : Performance maximale, moins de flexibilité
dyn: Flexibilité maximale, léger overhead
Ce que dyn dit vraiment
dyn signifie "dynamic" et permet le polymorphisme à l'exécution via les vtables
L'opposé de dyn est le polymorphisme statique (génériques, impl Trait)
Vec<dyn Trait> est impossible car dyn Trait n'a pas de taille connue à la compilation
Utilise Box<dyn Trait>, &dyn Trait ou des enums comme alternatives pratiques
Impact Réel : Cette compréhension permet d'architecturer des systèmes Rust efficaces qui utilisent le bon type de polymorphisme selon les besoins - statique pour la performance critique, dynamique pour l'extensibilité et l'abstraction runtime.
À lire ensuite
- Étape 33 sur 59•8 minLes supertraits : imposer une hiérarchie de comportements
Exploiter les supertraits pour établir des hiérarchies de comportements et les combiner avec des clauses where pour optimiser des algorithmes génériques complexes pour la performance et la sécurité de type
- Étape 34 sur 59•8 min?Sized : une seule fonction pour les types sized et unsized
- Étape 35 sur 59•9 minTypes associés ou génériques dans une API de driver I/O