Menu
Coddy logo textTech

Dispatch dynamique

Fait partie de la section Object Oriented Programming du Journey Rust de Coddy — leçon 45 sur 61.

Lorsque vous utilisez des génériques avec des bornes de trait comme <T: Summary>, Rust détermine le type exact au moment de la compilation. C'est ce qu'on appelle la résolution statique — le compilateur génère du code spécialisé pour chaque type concret que vous utilisez. C'est rapide, mais il y a une limitation : une variable ne peut contenir qu'un seul type spécifique.

Et si vous aviez besoin d'une seule variable capable de contenir différents types au moment de l'exécution ? C'est là que les objets de trait entrent en jeu. En utilisant Box<dyn Trait>, vous pouvez stocker n'importe quel type qui implémente le trait :

trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> String { String::from("Woof!") }
}

impl Speak for Cat {
    fn speak(&self) -> String { String::from("Meow!") }
}

fn main() {
    let animal: Box<dyn Speak> = Box::new(Dog);
    println!("{}", animal.speak());  // "Woof!"
    
    let animal: Box<dyn Speak> = Box::new(Cat);
    println!("{}", animal.speak());  // "Meow!"
}

Le mot-clé dyn indique une répartition dynamique — Rust recherche quelle méthode appeler au moment de l'exécution plutôt qu'au moment de la compilation. Le Box est nécessaire car les objets de trait n'ont pas de taille connue ; Box fournit un pointeur avec une taille fixe.

Voyez les choses ainsi : les génériques disent « Je travaille avec le type T », tandis que les objets de trait disent « Je travaille avec n'importe quoi capable de faire ceci ». Le compromis est un léger coût à l'exécution en échange de la flexibilité de manipuler différents types via la même variable.

challenge icon

Défi

Facile

Construisons un système de location de véhicules qui démontre la puissance de la répartition dynamique (dynamic dispatch) ! Vous allez créer un trait qui définit ce que tout véhicule louable peut faire, puis utiliser Box<dyn Trait> pour stocker différents types de véhicules dans la même variable.

Vous organiserez votre code sur deux fichiers :

  • vehicle.rs : Définissez un trait public Rentable avec une méthode rental_info qui prend &self et retourne un String. Créez ensuite deux structures publiques :
    • Car — avec un champ public model (String). Son rental_info doit retourner "Car: {model}"
    • Bike — avec un champ public brand (String). Son rental_info doit retourner "Bike: {brand}"
  • main.rs : Importez votre module vehicle et utilisez les entrées pour démontrer la répartition dynamique. Créez une variable Box<dyn Rentable> qui contient d'abord une Car, affichez ses informations de location, puis réassignez la même variable pour contenir un Bike et affichez à nouveau ses informations de location.

La magie ici est qu'une seule variable de type Box<dyn Rentable> peut contenir soit une Car soit un Bike—Rust détermine quelle méthode rental_info appeler au moment de l'exécution. C'est la répartition dynamique en action !

Votre sortie doit afficher les informations des deux véhicules :

Car: {model}
Bike: {brand}

Par exemple, avec les entrées Tesla Model 3 et Trek :

Car: Tesla Model 3
Bike: Trek

Vous recevrez deux entrées : le modèle de la voiture et la marque du vélo.

Aide-mémoire

Rust utilise le dispatch statique avec les génériques — le compilateur génère du code spécialisé pour chaque type au moment de la compilation. C'est rapide, mais cela signifie qu'une variable ne peut contenir qu'un seul type spécifique.

Les objets de trait permettent le dispatch dynamique, autorisant une seule variable à contenir différents types au moment de l'exécution. Utilisez Box<dyn Trait> pour stocker n'importe quel type implémentant le trait :

trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> String { String::from("Woof!") }
}

impl Speak for Cat {
    fn speak(&self) -> String { String::from("Meow!") }
}

fn main() {
    let animal: Box<dyn Speak> = Box::new(Dog);
    println!("{}", animal.speak());  // "Woof!"
    
    let animal: Box<dyn Speak> = Box::new(Cat);
    println!("{}", animal.speak());  // "Meow!"
}

Le mot-clé dyn indique un dispatch dynamique — les appels de méthodes sont résolus à l'exécution. Box est nécessaire car les objets de trait n'ont pas de taille connue à la compilation.

Différence clé : Les génériques disent « je travaille avec le type T » (un type spécifique), tandis que les objets de trait disent « je travaille avec n'importe quoi qui implémente ce trait » (plusieurs types via la même variable). Le compromis est un léger coût à l'exécution en échange de la flexibilité.

Essayez vous-même

mod vehicle;

use vehicle::{Rentable, Car, Bike};

fn main() {
    // Lire l'entrée
    let mut car_model = String::new();
    std::io::stdin().read_line(&mut car_model).expect("Failed to read line");
    let car_model = car_model.trim().to_string();
    
    let mut bike_brand = String::new();
    std::io::stdin().read_line(&mut bike_brand).expect("Failed to read line");
    let bike_brand = bike_brand.trim().to_string();
    
    // TODO : Créer une variable Box<dyn Rentable> qui contient une Car
    // Afficher ses informations de location en utilisant la méthode rental_info()
    
    // TODO : Réassigner la même variable pour qu'elle contienne un Bike
    // Afficher à nouveau ses informations de location
    
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Object Oriented Programming