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.
Défi
FacileConstruisons 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 publicRentableavec une méthoderental_infoqui prend&selfet retourne unString. Créez ensuite deux structures publiques :Car— avec un champ publicmodel(String). Sonrental_infodoit retourner"Car: {model}"Bike— avec un champ publicbrand(String). Sonrental_infodoit retourner"Bike: {brand}"
main.rs: Importez votre module vehicle et utilisez les entrées pour démontrer la répartition dynamique. Créez une variableBox<dyn Rentable>qui contient d'abord uneCar, affichez ses informations de location, puis réassignez la même variable pour contenir unBikeet 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: TrekVous 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
}
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
1Méthodes et comportement
Introduction aux blocs d'implémentationLe paramètre SelfMéthodes mutablesFonctions associéesBlocs d'implémentation multiplesChaînage de méthodesRécapitulatif - Actions sur le rectangle4Projet : Animal virtuel
Définir l'animalNourrir l'animal7Traits standards
Le trait DebugLe trait DisplayClone et CopyLes traits d'égalitéRécapitulatif - Point affichable10Projet : Système de documents
Le trait DrawComposant texte2Encapsulation et modules
Bases des modulesLe mot-clé publicChamps privésGettersSettersRécapitulatif - Casier sécurisé5Généricité
Structures génériquesMéthodes génériquesPlusieurs types génériquesFonctions génériquesRécapitulatif - Point de coordonnées8Les Traits comme contraintes
Syntaxe des contraintes de traitContraintes multiplesLa clause whereRetourner des types avec des traitsRécapitulatif - Afficheur générique11Design Patterns en Rust
Le pattern NewtypeCompositionLe trait DropFrom et IntoRécapitulatif - Mock de Smart Pointer3Enums avancées
Enums avec donnéesMéthodes sur les EnumsMatching des variantes de donnéesL'Enum Option revisitéeRécapitulatif - Enum Shape6Définition des Traits
Qu'est-ce qu'un Trait ?Implémenter des TraitsImplémentations par défautRedéfinir les comportements par défautTraits avec paramètresRécapitulatif - Lecteur multimédia9Objets de trait
Dispatch dynamiqueVecteurs de traitsItérer sur des objets de traitRécapitulatif - Gestionnaire de zoo