Los genéricos permiten escribir código altamente reutilizable mediante la parametrización de tipos. En lugar de duplicar lógica para diferentes tipos de datos, se utilizan “parámetros de tipo” (como <T>) que actúan como marcadores de posición que el compilador sustituirá por tipos reales. Esto es fundamental para estructuras de datos universales como Option<T> o Result<T, E>.

Existen tres formas principales de aplicar genéricos:
1. Funciones: Una función puede recibir parámetros de un mismo tipo genérico o de tipos distintos (ej. fn par<T, U>(a: T, b: U)).
2. Tipos (Structs/Enums): Permiten definir estructuras cuya composición depende de tipos que se definen al instanciar el objeto.
3. Implementaciones (impl): Puedes definir métodos para todos los tipos de un struct genérico, o crear implementaciones condicionales mediante Trait Bounds. Estos últimos permiten restringir un genérico para que solo funcione con tipos que implementen ciertas capacidades (como Display para imprimir o PartialOrd para comparar).

Un concepto crítico es la monomorfización. A diferencia de Java, donde los genéricos se “borran” (type erasure) y se usan objetos (Object), Rust genera una copia única y específica de la función o struct para cada tipo concreto que se utilice en el programa. Esto garantiza un rendimiento de “costo cero” (zero-cost abstraction), ya que el código generado es tan eficiente como si hubieras escrito versiones manuales para cada tipo, aunque esto puede aumentar ligeramente el tamaño del binario final.

use std::fmt::Display;

// Struct genérico con un único parámetro de tipo T
struct Contenedor<T> {
    valor: T,
}

// Implementación general para cualquier tipo T
impl<T> Contenedor<T> {
    fn nuevo(v: T) -> Self {
        Self { valor: v }
    }
}

// Implementación condicional: solo disponible si T implementa Display
impl<T: Display> Contenedor<T> {
    fn mostrar(&self) {
        println!("El valor es: {}", self.valor);
    }
}

// Función con dos parámetros de tipo distintos: T y U
fn transformar<T, U>(a: T, b: U) -> (T, U) {
    (a, b)
}

// Función con Trait Bound para permitir comparación (PartialOrd)
fn encontrar_maximo<T: PartialOrd>(lista: &[T]) -> &T {
    lista.iter().max().unwrap()
}

fn main() {
    // Uso de Contenedor con i32 (usa la implementación general)
    let c_num = Contenedor::nuevo(100);
    
    // Uso de Contenedor con &str (usa la implementación condicional por Display)
    let c_str = Contenedor::nuevo("Rust");
    c_str.mostrar();

    // Uso de transformar con tipos mixtos (i32 y &str)
    let tupla = transformar(42, "Genérico");
    println!("Tupla: {}, {}", tupla.0, tupla.1);

    // Uso de encontrar_maximo con f64
    let numeros = vec![1.2, 5.9, 3.3, 4.4];
    let max = encontrar_maximo(&numeros);
    println!("El máximo es: {}", max);
}

Explicación del Código

  • struct Contenedor<T>: Define una estructura con un parámetro de tipo genérico T.
  • impl<T> Contenedor<T>: Este bloque de implementación es general; permite llamar a nuevo(v: T) para cualquier tipo que se le asigne.
  • impl<T: Display> Contenedor<T>: Aquí se aplica un Trait Bound. El método mostrar solo existe para instancias de Contenedor cuyo tipo T implemente el trait Display. Si intentamos usar mostrar con un tipo que no puede imprimirse, el compilador lanzará un error.
  • fn transformar<T, U>(...): Demuestra la capacidad de manejar múltiples parámetros de tipo independientes (T y U), permitiendo que la función acepte un entero y un string simultáneamente.
  • fn encontrar_maximo<T: PartialOrd>(...): Restringe el tipo genérico T mediante el bound PartialOrd, lo cual es necesario para llamar al método .max() del iterador, ya que este requiere que los elementos puedan compararse.
  • En main, el compilador realiza la monomorfización: para c_num, genera una versión de Contenedor para i32; para c_str, genera otra versión específica para &str.

51