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?
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:
Self
donné il y a un seul Return
associé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érenceAdd
utilise un type associé car son auteur pensait qu'étant donné les deux arguments, il y aurait un type de retour logiqueComme 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é.
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é.