ProgrammationDéveloppeur Backend

Expliquez le mécanisme de dispatch dynamique des méthodes par trait object et en quoi il diffère du dispatch statique en Rust.

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Le dispatch est un mécanisme de sélection d'une fonction (méthode) spécifique à appeler. En Rust, il existe deux approches : le dispatch statique et le dispatch dynamique.

Contexte :

Dans les langages orientés objet, un appel de méthode dynamique utilise généralement une vtable (table virtuelle). En Rust, un mécanisme similaire est mis en œuvre pour les trait objects — des références à des objets de types implémentant certains traits. Le dispatch statique apparaît lors de l'utilisation de génériques et de limites de traits.

Problème :

Il faut souvent choisir entre la flexibilité (la capacité à travailler avec des objets de types différents via une interface unique) et la performance (le dispatch statique permet d'inliner des méthodes). Un choix incorrect conduit soit à des génériques excessivement compliqués, soit à des pertes de performance.

Solution :

Le dispatch statique est atteint via des paramètres génériques : dans ce cas, le compilateur génère un code distinct pour chaque type. Le dispatch dynamique se produit si une fonction prend un argument de type &dyn Trait ou Box<dyn Trait>, alors lors de l'appel de méthode sur le trait, Rust consulte la vtable à l'adresse, comme dans les langages OOP classiques.

Exemple de code :

trait Shape { fn area(&self) -> f64; } impl Shape for Circle { fn area(&self) -> f64 { 3.1415 * self.radius * self.radius } } fn print_area(shape: &dyn Shape) { // dispatch dynamique println!("area = {}", shape.area()); } // Ou statiquement : fn print_area_static<S: Shape>(shape: &S) { println!("area = {}", shape.area()); }

Caractéristiques clés :

  • dyn Trait utilise vtable (dispatch dynamique)
  • Les génériques sont appelés au moment de la compilation (dispatch statique)
  • Travaillent avec différents compromis sur la vitesse et la flexibilité

Questions pièges.

Peut-on faire Box<dyn Sized>?

Non. dyn Trait est par définition — unsized, il nécessite toujours l'utilisation de Box, Arc ou des références, mais pas «Box<dyn Sized>» — cela n'a pas de sens. Les trait objects n'ont pas le trait Sized.

Dyn Trait est-il autorisé pour les traits avec des méthodes génériques ?

Non. Il n'est pas possible de créer des traits object-safe avec des méthodes génériques (souvent confondu !) ; les types composites ne sont pas safety-object :

trait MyTrait { fn foo<T>(&self, x: T); } let x: &dyn MyTrait = ... // Erreur de compilation !

Peut-on faire dyn Trait pour un trait avec des valeurs Self dans la signature ?

Non, si la méthode renvoie Self (beaucoup de gens ne comprennent pas ce point : la sécurité des objets exige qu'il n'y ait pas de Self dans la signature ; on peut utiliser self uniquement dans les arguments, mais pas dans le retour).

Erreurs typiques et anti-patterns

  • Abus de dyn Trait là où le dispatch statique est approprié
  • Tentatives d'utiliser des méthodes génériques ou des Types Associés avec dyn Trait (le compilateur refusera)
  • Fuites de performance peu évidentes dans des endroits «fins» (appels fréquents)

Exemple de la vie réelle

Cas négatif

Partout, nous avons utilisé dyn Trait pour l'universalité des interfaces, même à l'intérieur de boucles serrées, où les génériques auraient suffi.

Avantages :

  • Flexibilité, extension facile de l'interface sans recompilation

Inconvénients :

  • Pertes de 15 à 30 % de performance sur les appels de méthode, impossibilité d'inlining

Cas positif

Le dispatch statique a été utilisé dans la logique interne, tandis que dyn Trait était utilisé uniquement aux frontières des modules.

Avantages :

  • Code le plus rapide possible à l'intérieur des modules
  • Flexibilité de l'API à la frontière publique

Inconvénients :

  • Nécessite une conception réfléchie de l'API, plus de fonctions génériques