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

109

Dans cette question , un problème est survenu qui pourrait être résolu en modifiant 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 la correspondance des traits en:

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

Le RFC utilise une structure graphique comme exemple motivant, et ceci est également utilisé dans la documentation , mais j'avoue ne pas apprécier pleinement les avantages de la version de type associée par rapport à la version paramétrée par type. Le principal est que la distanceméthode n'a pas besoin de se soucier du Edgetype. C'est bien, mais cela semble une raison un peu superficielle pour avoir des types associés.

J'ai trouvé que les types associés sont assez intuitifs à utiliser dans la pratique, mais j'ai du mal à décider 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?

Shepmaster
la source

Réponses:

76

Ceci est maintenant abordé dans la deuxième édition de The 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 de trait?

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 de mise en œuvre / performance, les deux extraits ci-dessus permettent à l'utilisateur de spécifier de manière dynamique comment hello_worlddoit se comporter.

La seule différence (sémantiquement) est que l' traitimplémentation garantit que pour un type donné Timplémentant le trait, hello_worldaura toujours le même comportement alors que lestruct implémentation 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 traitméthodes ci-dessus, un type associé est une forme de liaison tardive (bien qu'elle se produise lors de la compilation), permettant à l'utilisateur de traitspécifier pour une instance donnée le type à remplacer. Ce n'est pas le seul moyen (d'où la question):

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

Ou:

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

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

  • le premier impose que pour un donné Selfil y ait un seulReturn associé
  • le second, au contraire, permet de mettre en œuvre MyTraitpour Selfde multiplesReturn

La forme la plus appropriée dépend de la pertinence d'appliquer l'unicité ou non. Par exemple:

  • Deref utilise un type associé car sans unicité, le compilateur deviendrait fou lors de l'inférence
  • Add utilise un type associé car son auteur pensait que compte tenu des deux arguments, il y aurait un type de retour logique

Comme vous pouvez le voir, s'il Derefs'agit d'un cas d'utilisation évident (contrainte technique), le cas de Addest moins clair: peut-être serait-il logique i32 + i32de céder l'un i32ou l' autre 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. Néanmoins, 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é de l'utilisation d'un paramètre de trait régulier ne sont pas évidents, je suggérez de commencer par un type associé.

Matthieu M.
la source
4
Permettez-moi de simplifier un peu: trait/struct MyTrait/MyStructautorise exactement un impl MyTrait forou impl MyStruct. trait MyTrait<Return>autorise plusieurs impls car il est générique. Returnpeut être de n'importe quel type. Les structures génériques sont les mêmes.
Paul-Sebastian Manole
2
Je trouve votre réponse beaucoup plus facile à comprendre que celle de "The Rust Programming Language"
drojf
"le premier impose que pour un Soi donné il y ait un seul Retour associé". Cela est vrai dans le sens immédiat, mais on pourrait bien sûr contourner cette restriction en sous-classant avec un trait générique. Peut-être que l'unicité ne peut être qu'une suggestion et non appliquée
joel
37

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

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

Steve Klabnik
la source
5
Il m'a fallu un certain temps pour comprendre. Pour moi, cela ressemble plus à définir plusieurs types à la fois: l'arête et le nœud n'ont pas de sens hors du graphique.
tafia