Un trait bound es una restricción que exige que un tipo genérico implemente ciertos comportamientos.
En el artículo de Genéricos vimos que podíamos definir una función fn algo<T>(x: T).
Pero si intentabas hacer println!("{}", x) dentro de esa función, el compilador te daba un error.
¿Por qué? Porque Rust es conservador. Si dices que T puede ser cualquier tipo, Rust asume el peor escenario: podría ser un tipo que no tiene implementada la forma de imprimirse por pantalla (el trait Display).
Para poder usar métodos o comportamientos sobre un tipo genérico, necesitamos restringirlo. Necesitamos decirle al compilador: “Acepto cualquier tipo T, siempre y cuando implemente este Trait”.
A esto se le llama Trait Bound (Límite o Restricción de Trait).
Resumen de sintaxis
| Necesidad | Sintaxis | Cuándo usarla |
|---|---|---|
| Simple | fn f(x: &impl Trait) | Argumentos simples y distintos. |
| Relación entre tipos | fn f<T: Trait>(x: &T, y: &T) | Cuando dos argumentos deben ser el mismo tipo. |
| Múltiples Traits | T: Trait1 + Trait2 | Cuando necesitas varias capacidades. |
| Legibilidad | where T: Trait | Cuando la firma se vuelve muy larga. |
| Tipos de retorno complejos | -> impl Trait | Iteradores y Closures. |
La forma sencilla: impl Trait
Esta sintaxis la vimos brevemente en el artículo anterior. Es lo que llamamos “Azúcar Sintáctico”. Es la forma más legible de decir “dame algo que se comporte así”.
use std::fmt::Display;
// Aceptamos "algo que se pueda mostrar"
fn imprimir(valor: &impl Display) {
println!("{}", valor);
}
fn main() {
imprimir(&5); // i32 implementa Display
imprimir("Hola"); // &str implementa Display
}Es genial para casos simples. Pero tiene una limitación: cada parámetro es independiente.
Si escribimos esto:
fn comparar(a: &impl Resumible, b: &impl Resumible) { ... }Podríamos pasarle un Tweet en el primer argumento y un Articulo en el segundo. Ambos son Resumible, pero son tipos distintos. ¿Y si queremos obligar a que sean el mismo tipo?
La forma explícita: trait bounds
Para tener control total, usamos la sintaxis completa de genéricos: <T: Trait>.
Se lee: “Para un tipo T que implemente Trait…”.
// T debe implementar Display
fn imprimir<T: Display>(valor: &T) {
println!("{}", valor);
}Exigir el mismo tipo
Esta sintaxis gana a impl Trait cuando queremos relacionar varios tipos. Si necesitamos una función que compare dos items, y queremos garantizar que no nos mezclen peras con manzanas:
// a y b deben ser del MISMO tipo T, y ese T debe ser Resumible
fn comparar_misma_clase<T: Resumible>(a: &T, b: &T) {
println!("Comparando {} con {}", a.resumir(), b.resumir());
}Combinar restricciones con +
¿Qué pasa si queremos un tipo que se pueda imprimir (Display) Y ADEMÁS se pueda resumir (Resumible)?
Usamos el operador +.
fn notificar<T: Resumible + Display>(item: &T) {
println!("Resumen: {}", item.resumir());
println!("Original: {}", item);
}Esto funciona tanto en la sintaxis de Trait Bounds como en la de impl Trait.
La cláusula where
Cuando empezamos a tener funciones complejas con varios genéricos y múltiples restricciones, la firma de la función se vuelve ilegible.
Mira este monstruo:
fn procesar<T: Display + Clone, U: Clone + Debug>(a: T, b: U) -> i32 {
// ...
}Cuesta ver dónde termina la definición y dónde empieza el cuerpo de la función.
Para solucionar esto, Rust nos permite mover las restricciones al final de la firma usando la palabra clave where.
fn procesar<T, U>(a: T, b: U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// Ahora el código está limpio y separado
0
}Funciona exactamente igual, es solo una mejora de legibilidad. Y en Rust, la legibilidad cuenta.
Devolver impl Trait
Hasta ahora hemos hablado de parámetros de entrada. Pero, ¿y si queremos devolver un tipo complejo sin escribir su nombre larguísimo?
Esto es especialmente útil con iteradores y closures. Los tipos de los adaptadores pueden ser muy largos y los tipos concretos de los closures no se pueden nombrar directamente.
Podemos decir: “Voy a devolver algo que implementa Iterator, no te preocupes por el tipo concreto”.
fn crear_iterador() -> impl Iterator<Item = i32> {
let v = vec![1, 2, 3];
v.into_iter().map(|x| x * 2)
}La restricción de impl Trait en retorno:
Si usas -> impl Trait, la función debe devolver un solo tipo concreto. No puedes devolver un Tweet en un if y un Articulo en un else, incluso si ambos implementan el Trait.
// ❌ Esto NO compila
fn noticia(es_corto: bool) -> impl Resumible {
if es_corto {
Tweet { ... }
} else {
Articulo { ... } // Error: tipos distintos
}
}Para devolver tipos concretos distintos bajo una misma interfaz podríamos usar, por ejemplo, un objeto de trait como Box<dyn Resumible>, aceptando despacho dinámico.