Los tipos genéricos (generics) permiten escribir código que no depende de un tipo específico. Se implementan utilizando la sintaxis de corchetes angulares:
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
Aquí T es un tipo genérico limitado por el trait PartialOrd.
Los parámetros genéricos se declaran a través de <T>, pero se pueden limitar mediante restricciones de trait usando dos puntos, por ejemplo, <T: Display>. Este es un medio para informar al compilador que solo aquellos tipos para los cuales se ha implementado el trait necesario pueden ser utilizados.
En Rust se distinguen dos formas de despacho para los generics:
Influencia en el código máquina: El uso de generics con restricciones de trait (sin dyn Trait) resulta en monomorfización: un aumento en el tamaño del binario, pero con la máxima velocidad. El uso de dyn Trait ahorra espacio en el binario, pero hay una caída en el rendimiento.
Pregunta: Hay una función
fn do_something<T: Debug>(value: &T)
¿El compilador creará una función separada do_something en código binario para cada tipo con el que se utilice, o utilizará una implementación universal?
Respuesta incorrecta típica: Se utilizará una sola función para todos los tipos gracias a la restricción de trait.
Respuesta correcta: El compilador crea copias separadas de esta función para cada tipo (monomorfización), ya que la restricción de trait no hace que la función genérica sea "universal" a través de la vtable. La universalidad aparece solo en dyn Trait (despacho dinámico).
Ejemplo:
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // Para cada llamada con un tipo diferente se creará su propia versión de la función
Historia
En un proyecto con grandes objetos genéricos se descubrió que el archivo binario era sustancialmente más grande de lo esperado. Más tarde se esclareció: la razón fue el uso amplio de funciones genéricas sin limitaciones. Las llamadas con decenas de tipos llevaron a un crecimiento exponencial del tamaño del archivo ejecutable (code bloat), lo cual se identificó solo en la compilación de lanzamiento en CI.
Historia
Uno de los desarrolladores tomó un parámetro genérico con una restricción de trait, creyendo que tal código funcionaba con "despacho dinámico". Esto llevó a un despilfarro de memoria en el servidor y una disminución del rendimiento debido al constante crecimiento del código y su almacenamiento en caché por parte del procesador.
Historia
En la biblioteca intentaron usar un trait genérico con el tipo Self (por ejemplo, trait Clone) como dyn Trait, lo que no es compatible en Rust y llevó a un error de compilación. Era necesario reescribir el interfaz explícitamente, de lo contrario, la API genérica no funcionaría en modo dinámico y el interfaz tendría que ser modificado a nivel de tiempo de compilación.