Generics en Rust: funciones y structs que funcionan con cualquier tipo

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP