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 Traiten parámetros: cada parámetro puede ser de tipo distinto.T: Traiten genéricos: todos los parámetros marcados con T son del mismo tipo.impl Traiten 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.
