Quando é apropriado usar um tipo associado em vez de um tipo genérico?

109

Em esta pergunta , surgiu uma questão que poderia ser resolvido alterando uma tentativa de utilizar um parâmetro de tipo genérico em um tipo associado. Isso levou à pergunta "Por que um tipo associado é mais apropriado aqui?", O que me fez querer saber mais.

O RFC que introduziu os tipos associados diz:

Este RFC esclarece a correspondência de características por:

  • Tratar todos os parâmetros de tipo de característica como tipos de entrada e
  • Fornecendo tipos associados, que são tipos de saída .

O RFC usa uma estrutura de gráfico como um exemplo motivador, e isso também é usado na documentação , mas admito que não apreciei totalmente os benefícios da versão de tipo associada sobre a versão parametrizada de tipo. O principal é que o distancemétodo não precisa se preocupar com o Edgetipo. Isso é bom, mas parece um motivo um pouco superficial para ter tipos associados.

Descobri que os tipos associados são bastante intuitivos de usar na prática, mas me encontro com dificuldades para decidir onde e quando devo usá-los em minha própria API.

Ao escrever o código, quando devo escolher um tipo associado em vez de um parâmetro de tipo genérico e quando devo fazer o oposto?

Shepmaster
fonte

Respostas:

76

Isso agora é abordado na segunda edição de The Rust Programming Language . No entanto, vamos mergulhar um pouco mais além.

Vamos começar com um exemplo mais simples.

Quando é apropriado usar um método de característica?

Existem várias maneiras de fornecer vinculação tardia :

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)
    }
}

Desconsiderando qualquer estratégia de implementação / desempenho, os dois trechos acima permitem que o usuário especifique de forma dinâmica como hello_worlddeve se comportar.

A única diferença (semanticamente) é que a traitimplementação garante que, para um determinado tipo que Timplementa o trait, hello_worldsempre terá o mesmo comportamento, enquanto ostruct implementação permite ter um comportamento diferente por instância.

Se usar um método é apropriado ou não depende do caso de uso!

Quando é apropriado usar um tipo associado?

De maneira semelhante aos traitmétodos acima, um tipo associado é uma forma de vinculação tardia (embora ocorra na compilação), permitindo que o usuário do traitespecifique para uma determinada instância que tipo substituir. Não é a única maneira (daí a questão):

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

Ou:

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

São equivalentes à ligação tardia dos métodos acima:

  • o primeiro reforça que para um dado Selfexiste um único Returnassociado
  • o segundo, em vez disso, permite a implementação MyTraitde Selfpara múltiplosReturn

Qual forma é mais apropriada depende se faz sentido impor a unicidade ou não. Por exemplo:

  • Deref usa um tipo associado porque sem unicidade o compilador enlouqueceria durante a inferência
  • Add usa um tipo associado porque seu autor pensou que dados os dois argumentos haveria um tipo de retorno lógico

Como você pode ver, embora Derefseja um caso de uso óbvio (restrição técnica), o caso de Addé menos claro: talvez faria sentido i32 + i32render um i32ou Complex<i32>dependendo do contexto? No entanto, o autor exerceu seu julgamento e decidiu que não era necessário sobrecarregar o tipo de retorno para acréscimos.

Minha posição pessoal é que não existe uma resposta certa. Ainda assim, além do argumento da unicidade, eu mencionaria que os tipos associados tornam o uso do traço mais fácil, pois diminuem o número de parâmetros que devem ser especificados, então, caso os benefícios da flexibilidade de usar um parâmetro de traço regular não sejam óbvios, I sugerir começar com um tipo associado.

Matthieu M.
fonte
4
Deixe-me tentar simplificar um pouco: trait/struct MyTrait/MyStructpermite exatamente um impl MyTrait forou impl MyStruct. trait MyTrait<Return>permite vários impls porque é genérico. Returnpode ser de qualquer tipo. As estruturas genéricas são iguais.
Paul-Sebastian Manole,
2
Acho sua resposta muito mais fácil de entender do que a de "The Rust Programming Language"
drojf
“o primeiro reforça que para um determinado Eu há um único Retorno associado”. Isso é verdade no sentido imediato, mas pode-se, é claro, contornar essa restrição criando uma subclasse com um traço genérico. Talvez a unicidade possa ser apenas uma sugestão, e não imposta
joel
37

Os tipos associados são um mecanismo de agrupamento , portanto, devem ser usados ​​quando fizer sentido agrupar os tipos.

O Graphtraço introduzido na documentação é um exemplo disso. Você deseja que um Graphseja genérico, mas uma vez que tenha um tipo específico de Graph, você não quer que os tipos Nodeou Edgevariem mais. Um indivíduo Graphnão vai querer variar esses tipos em uma única implementação e, na verdade, deseja que sejam sempre os mesmos. Eles estão agrupados ou, pode-se dizer, associados .

Steve Klabnik
fonte
5
Levei algum tempo para entender. Para mim, parece mais definir vários tipos de uma vez: o Edge e o Node não fazem sentido no gráfico.
tafia de