Trait bounds en Rust: restringir qué tipos acepta una función genérica

Los trait bounds son restricciones que le dices al compilador sobre qué tipos acepta un parámetro genérico. Si tu función necesita comparar valores, el tipo T debe implementar PartialOrd. Si necesita imprimirlos, debe implementar Display. El compilador lo verifica en tiempo de compilación: no hay sorpresas en runtime.

Sintaxis básica: T: Trait

use std::fmt::Display;

fn imprimir_si_mayor<T: PartialOrd + Display>(a: T, b: T) {
    if a > b {
        println!("{} es mayor que {}", a, b);
    }
}

fn main() {
    imprimir_si_mayor(10, 5);       // i32: implementa PartialOrd + Display
    imprimir_si_mayor("z", "a");    // &str: implementa PartialOrd + Display
}

El + combina múltiples trait bounds. T: PartialOrd + Display significa que T debe implementar ambos.

La cláusula where

Cuando los bounds se complican, where mejora la legibilidad:

// Sin where: difícil de leer
fn comparar<T: PartialOrd + Display, U: Display + Clone>(a: T, b: T, extra: U) -> String {
    format!("{} {} {}", a, b, extra)
}

// Con where: mucho más legible
fn comparar<T, U>(a: T, b: T, extra: U) -> String
where
    T: PartialOrd + Display,
    U: Display + Clone,
{
    format!("{} {} {}", a, b, extra)
}

impl Trait en parámetros frente a T: Trait

// Equivalentes en comportamiento (ambos monomorfizan)
fn notificar_a(item: &impl Resumen) { /* ... */ }
fn notificar_b<T: Resumen>(item: &T) { /* ... */ }

// La diferencia: con genéricos puedes referenciar el tipo en múltiples lugares
fn comparar_resumenes<T: Resumen>(a: &T, b: &T) {
    // a y b deben ser del MISMO tipo T
}

fn comparar_resumenes_v2(a: &impl Resumen, b: &impl Resumen) {
    // a y b pueden ser de tipos DISTINTOS que implementen Resumen
}

impl Trait en retorno

fn crear_sumador() -> impl Fn(i32) -> i32 {
    let incremento = 5;
    move |x| x + incremento
}

fn main() {
    let suma5 = crear_sumador();
    println!("{}", suma5(10)); // 15
}

En retorno, impl Trait oculta el tipo concreto al llamador. Útil para closures y iteradores cuyo tipo concreto es muy largo o privado.

Limitación: solo puedes retornar un único tipo concreto. Si tienes ramas que retornan tipos distintos (ambos implementan el trait), necesitas Box<dyn Trait>.

Blanket implementations

Las blanket implementations implementan un trait para todos los tipos que cumplan ciertos bounds:

// De la stdlib: implementa ToString para cualquier T que implemente Display
impl<T: std::fmt::Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

// Por eso puedes hacer:
let s = 42.to_string();       // i32 implementa Display ? tiene to_string()
let s = 3.14.to_string();     // f64 también

Bounds en métodos de impl

struct Contenedor<T>(T);

impl<T> Contenedor<T> {
    fn obtener(&self) -> &T {
        &self.0
    }
}

// Método adicional solo para tipos que implementan Display
impl<T: Display> Contenedor<T> {
    fn mostrar(&self) {
        println!("Valor: {}", self.0);
    }
}

Resumen

  • T: Trait: restringe T a tipos que implementen Trait.
  • T: Trait1 + Trait2: múltiples restricciones.
  • where T: Trait: sintaxis alternativa para bounds complejos.
  • impl Trait en parámetros: cada parámetro puede ser de tipo distinto.
  • T: Trait en genéricos: todos los parámetros marcados con T son del mismo tipo.
  • impl Trait en retorno: oculta el tipo concreto.
  • Blanket implementations: implementa un trait para todos los T que cumplan un bound.

El siguiente artículo profundiza en los lifetimes: la forma de decirle al compilador cuánto tiempo deben vivir las referencias, algo que no siempre puede inferir por sí solo.

COMPARTE ESTE ARTÍCULO

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