web-dev-qa-db-fra.com

Quand est-il approprié d'utiliser un type associé par rapport à un type générique?

Dans cette question , un problème est survenu qui pourrait être résolu en changeant une tentative d'utilisation d'un paramètre de type générique en un type associé. Cela a suscité la question "Pourquoi un type associé est-il plus approprié ici?", Ce qui m'a donné envie d'en savoir plus.

Le RFC qui a introduit les types associés dit:

Cette RFC clarifie l'appariement des traits par:

  • Traiter tous les paramètres de type de trait comme types d'entrée , et
  • Fournir les types associés, qui sont les types de sortie .

Le RFC utilise une structure de graphique comme exemple de motivation, et cela est également utilisé dans la documentation , mais je reconnais ne pas apprécier pleinement les avantages de la version de type associée par rapport à la version paramétrée par type . La chose principale est que la méthode distance n'a pas besoin de se soucier du type Edge. C'est bien, mais cela semble un peu superficiel pour avoir des types associés.

J'ai trouvé que les types associés étaient assez intuitifs à utiliser dans la pratique, mais je me retrouve en difficulté quand je décide où et quand je dois les utiliser dans ma propre API.

Lors de l'écriture de code, quand dois-je choisir un type associé plutôt qu'un paramètre de type générique, et quand dois-je faire le contraire?

78
Shepmaster

Ceci est maintenant abordé dans la deuxième édition de Le Rust Programming Language . Cependant , plongeons un peu en plus.

Commençons par un exemple plus simple.

Quand est-il approprié d'utiliser une méthode des traits?

Il existe plusieurs façons de fournir une liaison tardive :

trait MyTrait {
    fn hello_Word(&self) -> String;
}

Ou:

struct MyTrait<T> {
    t: T,
    hello_world: fn(&T) -> String,
}

impl<T> MyTrait<T> {
    fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>;

    fn hello_world(&self) -> String {
        (self.hello_world)(self.t)
    }
}

Indépendamment de toute stratégie d'implémentation/de performance, les deux extraits ci-dessus permettent à l'utilisateur de spécifier de manière dynamique comment hello_world Devrait se comporter.

La seule différence (sémantique) est que l'implémentation trait garantit que pour un type donné T implémentant le trait, hello_world aura toujours le même comportement alors que l'implémentation struct permet d'avoir un comportement différent sur une base par instance.

Que l'utilisation d'une méthode soit appropriée ou non dépend du cas d'utilisation!

Quand est-il approprié d'utiliser un type associé?

De la même manière que les méthodes trait ci-dessus, un type associé est une forme de liaison tardive (bien qu'il se produise lors de la compilation), permettant à l'utilisateur de trait de spécifier pour une instance donnée le type à remplacer . Ce n'est pas le seul moyen (donc la question):

trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

Ou:

trait MyTrait<Return> {
    fn hello_world(&Self) -> Return;
}

Sont équivalentes à la liaison tardive des méthodes ci-dessus:

  • le premier impose que pour un Self donné il y a un seul Return associé
  • le second, à la place, permet d'implémenter MyTrait pour Self pour plusieurs Return

La forme la plus appropriée dépend de l’opportunité d’imposer l’unicité ou non. Par exemple:

  • Deref utilise un type associé car sans l'unicité le compilateur deviendrait fou pendant l'inférence
  • Add utilise un type associé car son auteur pensait qu'étant donné les deux arguments, il y aurait un type de retour logique

Comme vous pouvez le voir, alors que Deref est un cas d'utilisation évident (contrainte technique), le cas de Add est moins clair: peut-être que cela aurait du sens pour i32 + i32 pour donner soit i32 ou Complex<i32> selon le contexte? Néanmoins, l'auteur a exercé son jugement et a décidé qu'il n'était pas nécessaire de surcharger le type de retour pour les ajouts.

Ma position personnelle est qu'il n'y a pas de bonne réponse. Pourtant, au-delà de l'argument d'unicité, je mentionnerais que les types associés facilitent l'utilisation du trait car ils diminuent le nombre de paramètres à spécifier, donc au cas où les avantages de la flexibilité d'utiliser un paramètre de trait régulier ne sont pas évidents, je suggérer de commencer par un type associé.

49
Matthieu M.

Les types associés sont un mécanisme de regroupement, ils doivent donc être utilisés lorsqu'il est judicieux de regrouper les types ensemble.

Le trait Graph introduit dans la documentation en est un exemple. Vous voulez qu'un Graph soit générique, mais une fois que vous avez un type spécifique de Graph, vous ne voulez pas que les types Node ou Edge varient plus. Un Graph particulier ne va pas vouloir varier ces types au sein d'une même implémentation, et en fait, il veut qu'ils soient toujours les mêmes. Ils sont regroupés, ou on pourrait même dire associé.

27
Steve Klabnik