30 novembre 2025•5 min

Le mot-clé `dyn` : Origines, Signification et Limitations

m
mayo

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.

  1. Étape 33 sur 59•8 min
    Les 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

  2. Étape 34 sur 59•8 min
    ?Sized : une seule fonction pour les types sized et unsized
  3. Étape 35 sur 59•9 min
    Types associés ou génériques dans une API de driver I/O
Retour au blog
Partager ::