Genéricos en Rust: ¿por qué no puedes vivir sin ellos?
\n\nCuando empiezas a escribir en Rust, parece que el sistema de tipos es tu enemigo. El compilador se queja de cada coma, y la palabra Generic te hace temblar las rodillas. Pero en realidad, los genéricos (tipos genéricos) son tu mejor amigo. Te permiten escribir código flexible, seguro y reutilizable sin perder rendimiento.
En este artículo, entenderemos qué son los generics en Rust, cómo usarlos en funciones, estructuras y traits. Verás que la programación genérica no es magia, sino una herramienta poderosa que hace que tu código sea más limpio y rápido.
\n\n¿Qué son los genéricos y para qué sirven?
\n\nImagina que escribes una función que encuentra el elemento máximo en un array. Si no existieran los genéricos, tendrías que escribir una implementación separada para i32, f64, String y así sucesivamente. Esto es lento, tedioso y viola el principio DRY (Don't Repeat Yourself).
Los genéricos resuelven este problema. Permiten parametrizar tipos: escribes una función que funciona con cualquier tipo de dato. El compilador genera las versiones de código necesarias para cada caso específico. Este proceso se llama monomorfización.
\n\nSintaxis de los genéricos: de lo simple a lo complejo
\n\n1. Funciones genéricas
\n\nEmpecemos con un ejemplo básico. Supongamos que queremos crear una función que devuelve el valor pasado (función identidad):
\n\nfn identity<T>(value: T) -> T {\n value\n}\n\nfn main() {\n let n = identity(42);\n let s = identity("hello");\n println!("{}, {}", n, s); // 42, hello\n}\n\nAquí, <T> es un parámetro de tipo. T puede ser cualquier tipo: número, cadena, estructura. Al llamarlo, el compilador deduce qué tipo usar. Esto no funciona mediante despacho dinámico (como en Java o Python), sino mediante la generación de código especializado en tiempo de compilación. Por eso Rust no sacrifica rendimiento.
2. Restricciones de tipo (Trait Bounds)
\n\nPero, ¿qué pasa si queremos no solo pasar un valor, sino, por ejemplo, comparar dos números? Simplemente <T> no funcionará, porque no todos los tipos tienen definida la operación de comparación. Necesitamos indicar que T debe implementar el trait PartialOrd:
fn max<T: PartialOrd>(a: T, b: T) -> T {\n if a > b { a } else { b }\n}\n\nfn main() {\n println!("{}", max(10, 20)); // 20\n println!("{}", max(3.14, 2.71)); // 3.14\n}\n\nLa notación T: PartialOrd es un trait bound. Le dice al compilador: «Usa solo aquellos tipos que tengan definido un orden de comparación». Intenta pasar dos cadenas a max y funcionará, porque String también implementa PartialOrd. Pero si pasas una estructura sin implementación, obtendrás un error de compilación.
3. Múltiples restricciones
\n\nA veces es necesario que un tipo satisfaga varios traits a la vez. Por ejemplo, que sea tanto comparable como clonable:
\n\nfn clone_and_max<T: PartialOrd + Clone>(a: T, b: T) -> T {\n let a_cloned = a.clone();\n let b_cloned = b.clone();\n if a_cloned > b_cloned { a } else { b }\n}\n\nLa sintaxis T: PartialOrd + Clone significa que el tipo debe implementar ambos traits. Esto es poderoso, pero no abuses: demasiadas restricciones dificultan la legibilidad.
Genéricos en estructuras y enumeraciones
\n\nEstructuras genéricas
\n\nLos genéricos funcionan muy bien no solo con funciones, sino también con tipos personalizados. Un ejemplo clásico es la estructura Point, que puede almacenar coordenadas de cualquier tipo:
struct Point<T> {\n x: T,\n y: T,\n}\n\nimpl<T> Point<T> {\n fn new(x: