En esta página
Generics: abstracciones de costo cero
Generics: escribir código reutilizable
Los generics (tipos genéricos) permiten escribir funciones, structs y enums que funcionan con múltiples tipos sin duplicar código. En Rust, los generics son una característica central del lenguaje y se usan en toda la biblioteca estándar: Vec<T>, Option<T>, Result<T, E>, HashMap<K, V>.
Funciones genéricas
El problema sin generics: necesitas una función por tipo.
// Sin generics: código duplicado
fn maximo_i32(lista: &[i32]) -> i32 {
let mut max = lista[0];
for &n in lista {
if n > max { max = n; }
}
max
}
fn maximo_f64(lista: &[f64]) -> f64 {
let mut max = lista[0];
for &n in lista {
if n > max { max = n; }
}
max
}Con generics, una sola función maneja cualquier tipo comparable:
// T es el parámetro de tipo genérico
// PartialOrd: T debe poder compararse con > y <
// Copy: T debe poder copiarse (para asignar max = lista[0])
fn maximo<T: PartialOrd + Copy>(lista: &[T]) -> T {
let mut max = lista[0];
for &item in lista.iter() {
if item > max {
max = item;
}
}
max
}
fn main() {
let enteros = vec![34, 50, 25, 100, 65];
println!("Máximo entero: {}", maximo(&enteros));
let decimales = [2.5, 1.7, 9.3, 4.1, 7.8];
println!("Máximo decimal: {}", maximo(&decimales));
let caracteres = ['y', 'm', 'a', 'q'];
println!("Máximo char: {}", maximo(&caracteres));
}Structs genéricos
#[derive(Debug, Clone)]
struct Pila<T> {
elementos: Vec<T>,
capacidad_maxima: usize,
}
impl<T> Pila<T> {
fn nueva(capacidad: usize) -> Self {
Pila {
elementos: Vec::with_capacity(capacidad),
capacidad_maxima: capacidad,
}
}
fn empujar(&mut self, valor: T) -> Result<(), &'static str> {
if self.elementos.len() >= self.capacidad_maxima {
return Err("Pila llena");
}
self.elementos.push(valor);
Ok(())
}
fn sacar(&mut self) -> Option<T> {
self.elementos.pop()
}
fn cima(&self) -> Option<&T> {
self.elementos.last()
}
fn esta_vacia(&self) -> bool {
self.elementos.is_empty()
}
fn len(&self) -> usize {
self.elementos.len()
}
}
// Implementación específica para tipos que implementan Display
impl<T: std::fmt::Display> Pila<T> {
fn mostrar(&self) {
print!("[Cima] ");
for el in self.elementos.iter().rev() {
print!("{el} ");
}
println!("[Base]");
}
}
fn main() {
let mut pila_nums: Pila<i32> = Pila::nueva(5);
pila_nums.empujar(1).unwrap();
pila_nums.empujar(2).unwrap();
pila_nums.empujar(3).unwrap();
pila_nums.mostrar();
println!("Sacar: {:?}", pila_nums.sacar());
println!("Cima: {:?}", pila_nums.cima());
let mut pila_textos: Pila<&str> = Pila::nueva(3);
pila_textos.empujar("hola").unwrap();
pila_textos.empujar("mundo").unwrap();
pila_textos.mostrar();
}Enums genéricos
Ya conoces los enums genéricos más importantes de la std:
// Así están definidos en la biblioteca estándar:
// enum Option<T> { Some(T), None }
// enum Result<T, E> { Ok(T), Err(E) }
// Tú también puedes crear enums genéricos:
#[derive(Debug)]
enum Arbol<T> {
Hoja(T),
Nodo {
valor: T,
izquierdo: Box<Arbol<T>>,
derecho: Box<Arbol<T>>,
},
}
impl<T: std::fmt::Display + PartialOrd> Arbol<T> {
fn contiene(&self, objetivo: &T) -> bool {
match self {
Arbol::Hoja(v) => v == objetivo,
Arbol::Nodo { valor, izquierdo, derecho } => {
valor == objetivo
|| izquierdo.contiene(objetivo)
|| derecho.contiene(objetivo)
}
}
}
}
fn main() {
let arbol = Arbol::Nodo {
valor: 10,
izquierdo: Box::new(Arbol::Nodo {
valor: 5,
izquierdo: Box::new(Arbol::Hoja(3)),
derecho: Box::new(Arbol::Hoja(7)),
}),
derecho: Box::new(Arbol::Hoja(15)),
};
println!("¿Contiene 7? {}", arbol.contiene(&7));
println!("¿Contiene 6? {}", arbol.contiene(&6));
}Bounds múltiples con where
Cuando los bounds se vuelven complejos, la cláusula where mejora la legibilidad:
use std::fmt::{Debug, Display};
use std::ops::Add;
// Sin where (difícil de leer):
fn procesar<T: Display + Debug + Clone + PartialOrd>(valor: T) -> T { valor }
// Con where (más claro):
fn serializar<T>(coleccion: &[T]) -> String
where
T: Display + Debug + Clone,
{
coleccion.iter()
.map(|item| format!("{item}"))
.collect::<Vec<_>>()
.join(", ")
}
// Múltiples parámetros genéricos con bounds distintos
fn mezclar<A, B, C>(a: A, b: B) -> C
where
A: Into<C>,
B: Into<C>,
C: Add<Output = C>,
{
a.into() + b.into()
}
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
println!("{}", serializar(&numeros));
let palabras = ["hola", "mundo", "rust"];
println!("{}", serializar(&palabras));
// mezclar i32 + i32 → i64
// (simplificado — en la práctica From/Into tienen restricciones)
let resultado: i64 = mezclar(10_i32, 20_i32);
println!("Resultado: {resultado}");
}Monomorphization: por qué los generics son gratis
Cuando el compilador compila código genérico, genera una versión especializada para cada tipo concreto que uses. Este proceso se llama monomorphization:
fn identidad<T>(valor: T) -> T { valor }
fn main() {
let n = identidad(42_i32); // El compilador genera identidad_i32
let s = identidad("hola"); // El compilador genera identidad_str
let f = identidad(3.14_f64); // El compilador genera identidad_f64
println!("{n} {s} {f}");
}El binario resultante contiene código optimizado específico para cada tipo — exactamente como si hubieras escrito tres funciones separadas. No hay overhead de vtable, no hay boxing, no hay costos en tiempo de ejecución.
Esto contrasta con lenguajes como Java/C# donde los generics usan boxing y pueden tener overhead, o con Go donde los generics también usan monomorphization pero en una versión más reciente del lenguaje.
Turbofish: especificar tipos explícitamente
A veces el compilador no puede inferir el tipo genérico. Usa la sintaxis "turbofish" ::<T>:
fn main() {
// Sin turbofish — el compilador necesita contexto
let numeros: Vec<i32> = Vec::new();
// Con turbofish en el método
let v = Vec::<i32>::new();
// En llamadas a funciones genéricas
let n = "42".parse::<i32>().unwrap();
let f = "3.14".parse::<f64>().unwrap();
// En collect
let cuadrados = (1..=5).map(|n| n * n).collect::<Vec<_>>();
println!("{:?}", cuadrados);
// En from
let s = String::from("hola");
let v2 = Vec::<u8>::from(s.as_bytes());
println!("{:?}", v2);
}Generics en bloques impl: implementaciones condicionales
Puedes implementar métodos solo para ciertos tipos genéricos:
use std::fmt::Display;
struct Envuelto<T> {
valor: T,
}
impl<T> Envuelto<T> {
fn nuevo(valor: T) -> Self {
Envuelto { valor }
}
fn obtener(&self) -> &T {
&self.valor
}
}
// Solo disponible cuando T implementa Display
impl<T: Display> Envuelto<T> {
fn mostrar(&self) {
println!("Envuelto: {}", self.valor);
}
}
// Solo disponible cuando T implementa Clone
impl<T: Clone> Envuelto<T> {
fn duplicar(&self) -> (T, T) {
(self.valor.clone(), self.valor.clone())
}
}
fn main() {
let w = Envuelto::nuevo(42);
w.mostrar(); // disponible: i32 impl Display
let (a, b) = w.duplicar(); // disponible: i32 impl Clone
println!("Duplicado: {a}, {b}");
let w2 = Envuelto::nuevo(vec![1, 2, 3]);
// w2.mostrar(); // Vec<i32> implementa Debug pero no Display por defecto
let (v1, v2) = w2.duplicar(); // disponible: Vec impl Clone
println!("{:?} {:?}", v1, v2);
}Con un sólido entendimiento de generics y traits, estás listo para la lección de manejo de errores — donde Result<T, E> y el operador ? brillan con toda su potencia.
// Función genérica: T debe implementar PartialOrd y Copy
fn maximo<T: PartialOrd + Copy>(lista: &[T]) -> T {
let mut max = lista[0];
for &item in lista.iter() {
if item > max {
max = item;
}
}
max
}
// Struct genérico
#[derive(Debug)]
struct Par<T, U> {
primero: T,
segundo: U,
}
impl<T: std::fmt::Display, U: std::fmt::Display> Par<T, U> {
fn nuevo(primero: T, segundo: U) -> Self {
Par { primero, segundo }
}
fn mostrar(&self) {
println!("({}, {})", self.primero, self.segundo);
}
}
// Implementación solo para tipos que son iguales
impl<T: std::fmt::Display + PartialOrd> Par<T, T> {
fn mayor(&self) -> &T {
if self.primero > self.segundo { &self.primero } else { &self.segundo }
}
}
fn main() {
let enteros = vec![34, 50, 25, 100, 65];
println!("Máximo: {}", maximo(&enteros));
let decimales = [2.5, 1.7, 9.3, 4.1];
println!("Máximo: {}", maximo(&decimales));
let p1 = Par::nuevo("hola", 42);
p1.mostrar();
let p2 = Par::nuevo(10_i32, 20_i32);
println!("Mayor: {}", p2.mayor());
}
Inicia sesión para guardar tu progreso