¿Cuándo es apropiado utilizar un tipo asociado frente a un tipo genérico?

108

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 de gráfico como ejemplo motivador, y esto también se usa en la documentación , pero admitiré 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 distancemétodo no necesita preocuparse por el Edgetipo. Esto es bueno, pero parece una razón un poco superficial para tener tipos asociados.

He descubierto que los tipos asociados son bastante intuitivos de usar en la práctica, pero me cuesta decidir dónde y cuándo debo usarlos en mi propia API.

Al escribir código, ¿cuándo debería elegir un tipo asociado sobre un parámetro de tipo genérico y cuándo debería hacer lo contrario?

Pastor
fuente

Respuestas:

75

Esto se aborda ahora en la segunda edición de The Rust Programming Language . Sin embargo, profundicemos un poco más.

Comencemos con un ejemplo más simple.

¿Cuándo es apropiado utilizar un método de rasgos?

Hay varias formas de proporcionar 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 / desempeño, ambos extractos anteriores permiten al usuario especificar de manera dinámica cómo hello_worlddebe comportarse.

La única diferencia (semánticamente) es que la traitimplementación garantiza que para un tipo dado que Timplementa el trait, hello_worldsiempre tendrá el mismo comportamiento, mientras que la structimplementación 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 utilizar un tipo asociado?

De manera similar a los traitmétodos anteriores, un tipo asociado es una forma de enlace tardío (aunque ocurre en la compilación), lo que permite al usuario traitespecificar 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 vinculación tardía de los métodos anteriores:

  • el primero impone que para un determinado Selfhay un único Returnasociado
  • la segunda, en cambio, permite la implementación MyTraitde Selfde múltiplesReturn

Qué forma es más apropiada depende de si tiene sentido aplicar 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, si bien Derefes un caso de uso obvio (restricción técnica), el caso de Addes menos claro: ¿tal vez tendría sentido i32 + i32ceder i32o Complex<i32>según el contexto? No obstante, el autor hizo uso de su criterio y decidió que no era necesario sobrecargar el tipo de devolución para las 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 la cantidad 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, sugiera comenzar con un tipo asociado.

Matthieu M.
fuente
4
Permítanme intentar simplificar un poco: trait/struct MyTrait/MyStructpermite exactamente uno impl MyTrait foro impl MyStruct. trait MyTrait<Return>permite múltiples correos electrónicos implporque es genérico. Returnpuede ser de cualquier tipo. Las estructuras genéricas son las mismas.
Paul-Sebastian Manole
2
Encuentro su respuesta mucho más fácil de entender que la de "El lenguaje de programación Rust"
drojf
"el primero impone que para un Yo dado hay un solo Retorno asociado". Esto es cierto en el sentido inmediato, pero, por supuesto, se podría evitar esta restricción subclasificando con un rasgo genérico. Tal vez unicidad sólo puede ser una sugerencia, y no forzadas
Joel
36

Los tipos asociados son un mecanismo de agrupación , por lo que deben usarse cuando tenga sentido agrupar tipos.

El Graphrasgo introducido en la documentación es un ejemplo de esto. Desea que un Graphsea ​​genérico, pero una vez que tenga un tipo específico Graph, no desea que los tipos Nodeo Edgevaríen más. Un particular Graphno querrá variar esos tipos dentro de una sola implementación y, de hecho, quiere que sean siempre iguales. Están agrupados, o incluso podría decirse asociados .

Steve Klabnik
fuente
4
Me tomó un tiempo entenderlo. Para mí, se parece más a definir varios tipos a la vez: el borde y el nodo no tienen sentido fuera del gráfico.
tafia