Introducción: ¿Qué son los Generics y por qué se necesitan en Rust?
Rust es un lenguaje de programación de sistemas que se destaca por su seguridad, rendimiento y estricto sistema de tipos. Una de las herramientas clave que permite escribir código flexible y reutilizable son los Generics (tipos genéricos).
Los Generics permiten crear funciones, estructuras, enumeraciones y métodos que pueden trabajar con diferentes tipos de datos sin necesidad de duplicar código. En lugar de escribir una función separada para i32, f64 o String, se escribe una única versión genérica.
Por ejemplo, sin Generics tendrías que crear tres funciones diferentes para ordenar tres tipos distintos. Con Generics, solo una. Esto no solo reduce la cantidad de código, sino que también aumenta su fiabilidad: el compilador verifica la corrección de los tipos en tiempo de compilación, no en tiempo de ejecución.
Parte principal: cómo funcionan los Generics en Rust
1. Funciones genéricas: escribe una vez, úsalas para cualquier tipo
La forma más sencilla de familiarizarse con los Generics son las funciones genéricas. Imagina que necesitas escribir una función que devuelva el mayor de dos valores. En lugar de escribirla para cada tipo por separado, usamos la genericidad:
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!("El número más grande: {}", result);
let chars = vec!['y', 'm', 'a', 'q']; let result = largest(&chars); println!("El carácter más grande: {}", result);}Aquí, <T: PartialOrd> significa que la función funciona con cualquier tipo T que admita comparación (el trait PartialOrd). Esto es una restricción de trait, un concepto importante que se tratará más adelante.
2. Estructuras y enumeraciones genéricas: flexibilidad en la definición de datos
Los Generics no solo se aplican a funciones, sino también a estructuras de datos. Un ejemplo clásico es la estructura Point, que puede almacenar coordenadas de diferentes tipos:
struct Point<T> { x: T, y: T,}
fn main() { let integer_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.0, y: 4.0 }; // let wont_work = Point { x: 5, y: 4.0 }; // ¡Error! Tipos diferentes}Si necesitas que x e y puedan ser de tipos diferentes, usa varios parámetros de genericidad:
struct Point<T, U> { x: T, y: U,}
fn main() { let mixed_point = Point { x: 5, y: 4.0 }; // ¡Funciona!}Lo mismo aplica para las enumeraciones. Los ejemplos más conocidos son Option<T> y Result<T, E>, que son genéricos:
enum Option<T> { Some(T), None,}
enum Result<T, E> { Ok(T), Err(E),}3. Métodos en estructuras genéricas: añadiendo lógica
Puedes definir métodos para estructuras genéricas usando el bloque impl. Además, puedes crear métodos que funcionen solo para tipos específicos:
struct Point<T> { x: T, y: T,}
// Métodos para todos los tipos Timpl<T> Point<T> { fn x(&self) -> &T { &self.x }}
// Método solo para el tipo f64impl Point<f64> { fn distance_from_origin(&self) -> f64 { (self.x.powi(2) + self.y.powi(2)).sqrt() }}
fn main() { let p = Point { x: 3.0, y: 4.0 }; println!("p.x = {}", p.x()); println!("Distancia al origen: {}", p.distance_
```