Introducción a los Generics en Rust
La programación genérica (generics) es uno de los conceptos clave de Rust, que permite escribir código flexible, reutilizable y seguro. Si estás migrando a Rust desde otros lenguajes (como C++ o Java), seguramente te has encontrado con plantillas o genéricos. En Rust, los generics funcionan de manera similar, pero con un enfoque especial en el rendimiento y la seguridad de la memoria.
¿Para qué sirven los generics? Imagina que necesitas escribir una función que encuentre el elemento máximo en una lista. Sin generics, tendrías que escribir una versión separada para i32, f64, String y cada tipo personalizado. Con generics, escribes una sola función que funciona con cualquier tipo que admita comparación.
En este artículo, analizaremos qué son los generics en Rust, cómo usarlos con funciones, estructuras y traits, y veremos ejemplos prácticos.
Fundamentos de Generics: funciones y estructuras
Funciones genéricas
La forma más sencilla de familiarizarse con los generics es escribir una función genérica. La sintaxis utiliza corchetes angulares <T>, donde T es un parámetro de tipo (type parameter).
fn largest<T>(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);}¡Sin embargo, este código no se compilará! Rust requiere que el tipo T implemente el trait PartialOrd (para comparación). Las restricciones (trait bounds) las veremos más adelante, pero por ahora recuerda: los generics permiten abstraerse del tipo concreto, pero el compilador debe saber qué operaciones están permitidas.
Estructuras genéricas
Los generics no solo funcionan con funciones, sino también con tipos de datos personalizados. Por ejemplo, un punto con coordenadas puede almacenar números enteros o números de punto flotante:
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 }; println!("Punto entero: ({}, {})", integer_point.x, integer_point.y); println!("Punto flotante: ({}, {})", float_point.x, float_point.y);}Se pueden usar múltiples parámetros de tipo si los campos tienen tipos diferentes:
struct Pair<T, U> { first: T, second: U,}
fn main() { let pair = Pair { first: 42, second: "respuesta" }; println!("Par: {} y {}", pair.first, pair.second);}Generics con traits y restricciones (Trait Bounds)
¿Qué son los trait bounds?
Los trait bounds son una forma de indicarle al compilador que un tipo genérico debe implementar ciertos traits. Sin ellos, no podemos llamar métodos sobre un valor genérico. Volvamos a la función largest y corrijámosla:
use std::cmp::PartialOrd;
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]; println!("El más grande: {}", largest(&numbers));}Ahora la función solo acepta tipos que admiten comparación (PartialOrd).
Restricciones múltiples
Puedes especificar varios traits usando +. Por ejemplo, el tipo debe ser tanto comparable como clonable:
use std::fmt::Display;
fn print_and_compare