Box de dyn Trait y colecciones heterogéneas
Box de dyn Trait posee un trait object en el heap y, con él, un vector puede guardar tipos concretos distintos que solo comparten un trait. Por qué un Vec de un tipo no puede ser heterogéneo y uno de trait objects sí, y los patrones donde esto brilla: plugins, comandos y fábricas.
Un &dyn Trait presta el valor; pero a menudo quieres poseerlo y, sobre todo, guardar muchos valores de tipos distintos juntos. Ahí entra Box<dyn Trait>: un trait object del que eres dueño, alojado en el heap. Con él, un Vec<Box<dyn Trait>> puede contener un círculo, un rectángulo y un texto codo con codo —la colección heterogénea que los genéricos, por sí solos, son incapaces de expresar—. Es, probablemente, la razón número uno por la que existen los trait objects.
- Poseer un trait object en el heap con
Box<dyn Trait>. - Construir un
Vec<Box<dyn Trait>>con valores de tipos concretos distintos. - Entender por qué un
Vec<T>no puede ser heterogéneo y unVec<Box<dyn T>>sí. - Reconocer los patrones donde esto brilla: plugins, comandos y fábricas.
Poseer un trait object: Box de dyn Trait
&dyn Trait es un préstamo: el valor vive en otra parte y tú solo tienes una vista. Pero muchas veces necesitas ser el dueño del valor borrado —guardarlo, moverlo, devolverlo de una función—. Como dyn Trait no tiene tamaño conocido, no puede vivir suelto en la pila; la solución idiomática es ponerlo en el heap y quedarte con el puntero: Box<dyn Trait>.
trait Forma {
fn area(&self) -> f64;
fn nombre(&self) -> &str;
}
struct Circulo { radio: f64 }
struct Rect { ancho: f64, alto: f64 }
impl Forma for Circulo {
fn area(&self) -> f64 { std::f64::consts::PI * self.radio * self.radio }
fn nombre(&self) -> &str { "circulo" }
}
impl Forma for Rect {
fn area(&self) -> f64 { self.ancho * self.alto }
fn nombre(&self) -> &str { "rectangulo" }
}
let forma: Box<dyn Forma> = Box::new(Circulo { radio: 1.0 });
println!("{}: {:.3}", forma.nombre(), forma.area());
Box::new(Circulo { .. }) reserva el círculo en el heap y devuelve un Box<Circulo>; al asignarlo a un Box<dyn Forma>, ocurre la coerción de unsizing de la lección 15.2: el Box pasa de puntero fino a puntero gordo, adjuntando la vtable de (Circulo, Forma).
La colección heterogénea
Aquí está el pago de todo el andamiaje. Como cada Box<dyn Forma> tiene el mismo tipo estático y el mismo tamaño —un puntero gordo—, puedes meter muchos en un solo Vec, aunque los valores a los que apuntan sean de tipos completamente distintos:
fn main() {
let formas: Vec<Box<dyn Forma>> = vec![
Box::new(Circulo { radio: 1.0 }),
Box::new(Rect { ancho: 2.0, alto: 3.0 }),
Box::new(Circulo { radio: 0.5 }),
];
let total: f64 = formas.iter().map(|f| f.area()).sum();
for f in &formas {
println!("{:<12} area = {:.3}", f.nombre(), f.area());
}
println!("area total = {total:.3}");
}
El Vec almacena tres punteros gordos idénticos en forma, distintos en contenido: dos apuntan a círculos, uno a un rectángulo, cada uno con la vtable de su tipo. Al iterar, f.area() despacha dinámicamente: para cada elemento consulta su vtable y salta al area correcto. Un solo bucle, comportamientos distintos.
flowchart TB vec[Vec de Box dyn Forma] --> b1[Box gordo 1] vec --> b2[Box gordo 2] vec --> b3[Box gordo 3] b1 --> c1[Circulo en heap] b1 --> vc[vtable Circulo y Forma] b2 --> r1[Rect en heap] b2 --> vr[vtable Rect y Forma] b3 --> c2[Circulo en heap] b3 --> vc style vec fill:#cba6f7,color:#11111b style vc fill:#89b4fa,color:#11111b style vr fill:#89b4fa,color:#11111b
Por qué los genéricos no bastan aquí
La tentación es preguntar: “¿y no puedo usar un Vec genérico?”. No, y ver por qué cierra el círculo de todo el nivel. Un Vec<T> fija un único T para todos sus elementos: todos idénticos en tipo y tamaño, elegido en compilación. Un Vec<impl Forma> es lo mismo: un solo tipo concreto. Ninguno admite mezcla.
// NO compila: un Vec exige un unico tipo para todos sus elementos.
// let v = vec![
// Circulo { radio: 1.0 },
// Rect { ancho: 2.0, alto: 3.0 }, // error: se esperaba Circulo, se encontro Rect
// ];
Para mezclar tipos que solo comparten un trait, tienes que borrar el tipo tras dyn; y para poseerlos dentro de un Vec, necesitas un manejador de tamaño uniforme: Box<dyn Forma>, el puntero gordo de tamaño fijo. Esta es, en una frase, la razón de existir de los trait objects: monomorfizar no tiene respuesta cuando no hay un único tipo que monomorfizar.
Patrones donde esto brilla
El Vec<Box<dyn Trait>> es la columna vertebral de varios patrones de diseño en Rust:
- Plugins y registros abiertos. Una lista de extensiones cuyo conjunto no controlas:
Vec<Box<dyn Plugin>>. Cada plugin es un tipo distinto, quizá en un crate distinto, unidos solo por el trait. - Patrón comando.
Vec<Box<dyn Comando>>como cola de acciones heterogéneas —guardar, deshacer, enviar— que se ejecutan con un mismo bucle. - Fábricas y retorno.
fn crear(cfg: &Config) -> Box<dyn Forma>devuelve uno de varios tipos concretos según la configuración, sin revelar cuál en la firma. - El caso que ya conoces.
Box<dyn Error>del nivel 7 es exactamente esto: un error poseído cuyo tipo concreto se borró, para que cualquier fallo quepa en el mismo canal.
El patrón fábrica, en código:
fn crear(clase: &str) -> Box<dyn Forma> {
match clase {
"circulo" => Box::new(Circulo { radio: 1.0 }),
_ => Box::new(Rect { ancho: 1.0, alto: 1.0 }),
}
}
La firma promete una Forma, pero cada rama devuelve un tipo concreto distinto. Sin Box<dyn Forma> esto no tendría un tipo de retorno posible: las ramas de un match deben coincidir en tipo, y Circulo y Rect solo coinciden una vez borrados tras el trait.
Si tu función devuelve siempre el mismo tipo concreto —aunque no quieras nombrarlo—, usa -> impl Forma: es dispatch estático, sin heap ni vtable. Reserva -> Box<dyn Forma> para cuando el tipo devuelto varía según la lógica, como en la fábrica de arriba. La regla: impl Trait cuando el tipo es único pero anónimo; Box<dyn Trait> cuando de verdad son varios.
Cuando necesitas compartir en vez de poseer en exclusiva, el mismo patrón vive con Rc<dyn Trait> en un hilo o Arc<dyn Trait> entre varios; y si solo quieres prestar, basta &dyn Trait.
La clave que hace posible todo es sutil: los valores detrás del dyn tienen tamaños distintos —un Circulo no ocupa lo mismo que un Rect—, pero el Box<dyn Forma> que los envuelve mide siempre lo mismo, dos palabras. El Vec nunca ve la irregularidad de los datos; solo ve una fila de punteros gordos idénticos en tamaño. Ese es el papel del Box: convertir tipos de tamaño dispar en manejadores de tamaño fijo que una colección homogénea puede alinear en memoria.
Mientras tus tipos sean homogéneos, los genéricos ganan siempre: monomorfizan, inlinan, cuestan cero. Pero el mundo real rara vez es homogéneo. Una escena gráfica mezcla círculos, textos y sprites; una tubería de procesamiento encadena etapas de tipos dispares; un editor guarda una pila de comandos que tú no diseñaste todos. En cuanto los datos son genuinamente heterogéneos y de conjunto abierto, la monomorfización se queda muda, porque su pregunta —“¿cuál es el tipo T?”— no tiene una sola respuesta: hay muchos, y algunos ni siquiera existían cuando compilaste tu biblioteca. El borrado de tipo es la única forma de que valores de tipos distintos compartan estante, y Box<dyn Trait> es ese estante: un manejador uniforme y poseído, cuyo tamaño fijo permite que un Vec lo contenga, y cuya vtable permite que cada elemento siga comportándose como lo que de verdad es. Pagas una indirección por llamada y renuncias al inlining, sí; pero a cambio compras algo que los genéricos no venden a ningún precio: heterogeneidad en tiempo de ejecución y un mundo abierto a tipos futuros. Por eso decir “el dispatch dinámico es más lento” es un error de categoría, como decir que un barco es un coche malo: hace otro trabajo. El día que tu Vec necesita contener cosas distintas que solo se parecen en lo que saben hacer, no hay debate de rendimiento que valga —solo dyn puede expresarlo—, y ese día entiendes que los trait objects no son la versión lenta de los genéricos, sino la respuesta a una pregunta que los genéricos no saben ni formular.
- Define
Formaconareaynombre, impleméntalo paraCirculoyRect, y crea unVec<Box<dyn Forma>>con varios de cada uno. - Recorre el vector sumando las áreas e imprimiendo cada nombre; identifica en qué punto exacto ocurre el dispatch dinámico.
- Intenta crear
vec![Circulo { .. }, Rect { .. }]sinBox<dyn ...>y lee el error; explica por qué unVecno admite la mezcla. - Escribe
fn crear(lado: f64, es_circulo: bool) -> Box<dyn Forma>que devuelva unCirculoo unRectsegún el booleano. - Cambia
Box<dyn Forma>porRc<dyn Forma>y comparte un mismo elemento en dos vectores; razona qué aportaRcqueBoxno.