types - keywords - meta tags generator
¿Cuándo es apropiado usar un tipo asociado versus un tipo genérico? (2)
En esta pregunta , surgió un problema que podría resolverse cambiando un intento de usar un parámetro de tipo genérico en un tipo asociado. Eso provocó la pregunta "¿Por qué un tipo asociado es más apropiado aquí?", Lo que me hizo querer saber más.
El RFC que introdujo los tipos asociados dice:
Este RFC aclara la coincidencia de rasgos mediante:
- Tratar todos los parámetros de tipo de rasgo como tipos de entrada , y
- Proporcionar tipos asociados, que son tipos de salida .
El RFC usa una estructura gráfica como un ejemplo motivador, y esto también se usa en
la documentación
, pero admito que no aprecio completamente los beneficios de la versión de tipo asociada sobre la versión con parámetros de tipo.
Lo principal es que el método de
distance
no necesita preocuparse por el tipo de
Edge
.
Esto es bueno, pero parece una razón un poco superficial para tener tipos asociados.
He encontrado que los tipos asociados son bastante intuitivos para usar en la práctica, pero me cuesta mucho decidir cuándo y dónde debo usarlos en mi propia API.
Al escribir código, ¿cuándo debo elegir un tipo asociado sobre un parámetro de tipo genérico y cuándo debo hacer lo contrario?
Esto se menciona ahora en la segunda edición de The Rust Programming Language . Sin embargo, vamos a sumergirnos un poco más.
Comencemos con un ejemplo más simple.
¿Cuándo es apropiado usar un método de rasgo?
Hay varias formas de proporcionar un enlace tardío :
trait MyTrait {
fn hello_word(&self) -> String;
}
O:
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)
}
}
Sin tener en cuenta ninguna estrategia de implementación / rendimiento, ambos extractos anteriores permiten al usuario especificar de manera dinámica cómo debe comportarse
hello_world
.
La única diferencia (semánticamente) es que la implementación del
trait
garantiza que para un tipo
T
dado que implementa el
trait
,
hello_world
siempre tendrá el mismo comportamiento, mientras que la implementación de la
struct
permite tener un comportamiento diferente por instancia.
¡Si usar un método es apropiado o no depende del caso de uso!
¿Cuándo es apropiado usar un tipo asociado?
De manera similar a los métodos de
trait
anteriores, un tipo asociado es una forma de enlace tardío (aunque ocurre en la compilación), lo que permite al usuario del
trait
especificar para una instancia determinada qué tipo sustituir.
No es la única forma (de ahí la pregunta):
trait MyTrait {
type Return;
fn hello_world(&self) -> Self::Return;
}
O:
trait MyTrait<Return> {
fn hello_world(&Self) -> Return;
}
Son equivalentes a la unión tardía de los métodos anteriores:
-
el primero hace cumplir que para un
Self
dado hay un soloReturn
asociado -
el segundo, en cambio, permite implementar
MyTrait
forSelf
paraReturn
múltiple
La forma más apropiada depende de si tiene sentido imponer la unicidad o no. Por ejemplo:
-
Deref
usa un tipo asociado porque sin unicidad el compilador se volvería loco durante la inferencia -
Add
usa un tipo asociado porque su autor pensó que dados los dos argumentos habría un tipo de retorno lógico
Como puede ver, mientras
Deref
es un caso de uso obvio (restricción técnica), el caso de
Add
es menos claro: ¿quizás tendría sentido que
i32 + i32
produzca
i32
o
Complex<i32>
dependiendo del contexto?
No obstante, el autor ejerció su juicio y decidió que no era necesario sobrecargar el tipo de devolución para adiciones.
Mi postura personal es que no hay una respuesta correcta. Aún así, más allá del argumento de la unicidad, mencionaría que los tipos asociados facilitan el uso del rasgo ya que disminuyen el número de parámetros que deben especificarse, por lo que en caso de que los beneficios de la flexibilidad de usar un parámetro de rasgo regular no sean obvios, yo sugiera comenzar con un tipo asociado.
Los tipos asociados son un mecanismo de agrupación , por lo que deben usarse cuando tenga sentido agrupar los tipos.
El rasgo
Graph
introducido en la documentación es un ejemplo de esto.
Desea que un
Graph
sea genérico, pero una vez que tiene un tipo específico de
Graph
, ya no desea que los tipos de
Node
o
Edge
varíen más.
Un
Graph
particular no va a querer variar esos tipos dentro de una sola implementación y, de hecho, quiere que siempre sean los mismos.
Están agrupados, o incluso se podría decir que están
asociados
.