Los generics permiten escribir código que funciona con múltiples tipos sin duplicarlo. En lugar de definir mayor_i32, mayor_f64 y mayor_char por separado, defines mayor<T> una sola vez. El compilador genera versiones especializadas para cada tipo que uses: zero overhead en runtime.
Función genérica
fn mayor<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", mayor(5, 10)); // i32
println!("{}", mayor(3.14, 2.71)); // f64
println!("{}", mayor('a', 'z')); // char
}
El parámetro de tipo T puede ser cualquier nombre, pero por convención se usa una letra mayúscula. T: PartialOrd es un bound: le dice al compilador que T debe implementar la capacidad de compararse con >.
Struct genérico
struct Par<T> {
primero: T,
segundo: T,
}
impl<T> Par<T> {
fn new(primero: T, segundo: T) -> Self {
Self { primero, segundo }
}
}
// Método solo disponible si T implementa Display y PartialOrd
impl<T: std::fmt::Display + PartialOrd> Par<T> {
fn mostrar_mayor(&self) {
if self.primero >= self.segundo {
println!("Mayor: {}", self.primero);
} else {
println!("Mayor: {}", self.segundo);
}
}
}
fn main() {
let par = Par::new(5, 10);
par.mostrar_mayor(); // Mayor: 10
}
Múltiples parámetros de tipo
struct Mapa<K, V> {
clave: K,
valor: V,
}
fn intercambiar<A, B>(a: A, b: B) -> (B, A) {
(b, a)
}
fn main() {
let (b, a) = intercambiar(1, "hola");
println!("{} {}", a, b); // 1 hola
}
Generics en enums: Option y Result
// Así están definidos en la stdlib
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Ya los has usado desde el primer artículo. Son generics ordinarios.
Monomorfización: zero overhead
Rust implementa los generics mediante monomorfización: en tiempo de compilación, genera una versión concreta del código para cada combinación de tipos que uses.
// Tu código genérico
fn identidad<T>(x: T) -> T { x }
// El compilador genera (conceptualmente):
fn identidad_i32(x: i32) -> i32 { x }
fn identidad_str(x: &str) -> &str { x }
// etc.
El resultado es que los generics no tienen ningún coste en runtime: el código generado es idéntico al que escribirías a mano con tipos concretos. La penalización es en tiempo de compilación (más código que compilar) y en tamaño del binario.
Restricciones en métodos específicos
use std::fmt;
struct Envoltorio<T>(T);
// Este método existe para cualquier T
impl<T> Envoltorio<T> {
fn inner(&self) -> &T {
&self.0
}
}
// Este método solo existe si T implementa Display
impl<T: fmt::Display> Envoltorio<T> {
fn mostrar(&self) {
println!("{}", self.0);
}
}
fn main() {
let w = Envoltorio(42);
w.mostrar(); // OK: i32 implementa Display
println!("{}", w.inner());
}
Resumen
fn f<T>(x: T): función genérica con parámetro de tipo T.struct S<T>: struct genérico.T: Trait: restricción sobre qué tipos acepta T (trait bound).- Múltiples parámetros:
fn f<A, B>(a: A, b: B). - Monomorfización: el compilador genera código especializado por tipo. Zero overhead en runtime.
El siguiente artículo profundiza en los traits: cómo definir comportamiento compartido entre tipos distintos, el equivalente de Rust a las interfaces de otros lenguajes.
