Generics en Rust: para qué sirven y cómo usarlos en la práctica
La programación genérica (Generics) es una de las capacidades clave de Rust que permite escribir código flexible, reutilizable y seguro. En este artículo, analizaremos para qué sirven los Generics, cómo funcionan internamente y cómo aplicarlos en proyectos reales. El material será útil tanto para principiantes que recién están aprendiendo Rust como para desarrolladores experimentados que deseen sistematizar sus conocimientos.
¿Para qué sirven los Generics en Rust?
Imagine que necesita escribir una función que encuentre el elemento más grande en un array. Sin Generics, tendría que crear versiones separadas para cada tipo de datos: i32, f64, String, etc. Esto lleva a la duplicación de código y dificulta el mantenimiento.
Los Generics resuelven este problema al permitir parametrizar tipos. En lugar de escribir:
fn find_max_i32(list: &[i32]) -> i32 { ... }fn find_max_f64(list: &[f64]) -> f64 { ... }Escribe una función universal:
fn find_max<T: PartialOrd>(list: &[T]) -> &T { ... }Las principales ventajas de los Generics:
- Reutilización de código — una función o estructura funciona con diferentes tipos
- Seguridad de tipos en tiempo de compilación — todos los errores de tipo se detectan antes de ejecutar el programa
- Rendimiento — Rust utiliza monomorfización, creando código optimizado para cada tipo específico
- Legibilidad — menos duplicación, el código se vuelve más conciso
Cómo funcionan los Generics: monomorfización
Rust compila el código genérico de una manera especial — mediante monomorfización. Esto significa que el compilador crea una copia separada de la función o estructura para cada tipo específico que se utiliza en el programa.
Ejemplo:
fn print_type<T>(val: T) { println!("Valor: {}", val);}
fn main() { print_type(42); // T = i32 print_type(3.14); // T = f64 print_type("Hola"); // T = &str}Después de la compilación, Rust creará tres versiones de print_type:
print_type_i32print_type_f64print_type_str
Esto garantiza que el código genérico no sea inferior en rendimiento al código especializado. La única "desventaja" es el aumento del tamaño del archivo binario, pero en la práctica esto rara vez se convierte en un problema.
Uso de Generics con funciones y estructuras
Generics en funciones
La forma más sencilla de usar Generics es parametrizar una función:
fn swap<T>(a: &mut T, b: &mut T) { let temp = std::mem::replace(a, unsafe { std::mem::zeroed() }); *a = std::mem::replace(b, temp); *b = temp;}
fn main() { let mut x = 10; let mut y = 20; swap(&mut x, &mut y); println!("x = {}, y = {}", x, y); // x = 20, y = 10}Tenga en cuenta: no especificamos el tipo — el compilador lo deduce automáticamente según los argumentos.
Generics en estructuras
Las estructuras también pueden ser genéricas. Esto es especialmente útil para contenedores de datos:
struct Point<T> { x: T, y: T,}
impl<T> Point<T> { fn new(x: T, y: T) -> Self { Point { x, y } }
fn distance_from_origin(&self) -> f64 where T: Into<f64> + Copy, { let x: f64 = self.x.into(); let y: f64 = self.y.into(); (x * x + y * y).sqrt() }}
fn main() { let int_point = Point::new(3, 4); let float_point = Point::new(3.0, 4.0); // distance_from_origin solo funciona para tipos que implementan Into<f6