El despacho dinámico en Rust, mediante dyn Trait, permite la polimorfismo en tiempo de ejecución, permitiendo tratar diferentes tipos como un único tipo genérico. A diferencia del despacho estático (impl Trait o genéricos), donde el compilador realiza la monomorfización (generando una copia de la función para cada tipo), el despacho dinámico utiliza fat pointers (punteros gordos).
Un &dyn Trait no es un simple puntero a una dirección de memoria; es una estructura de dos palabras (en sistemas de 64 bits, 16 bytes) compuesta por:
1. Puntero de datos: La dirección de memoria de la instancia concreta en el heap o el stack.
2. Puntero a la vtable: La dirección de una tabla de métodos virtuales (virtual method table).
La vtable es una estructura de datos generada en tiempo de compilación que contiene información crucial: el puntero al destructor (drop_in_place), el tamaño del tipo concreto, su alineación y, finalmente, una lista de punteros a las implementaciones de cada método del trait para ese tipo específico. El costo de esta arquitectura es la indirección doble: para ejecutar un método, el procesador debe seguir el puntero de datos para encontrar la vtable, y luego seguir el puntero de la vtable para encontrar la dirección del método. Esto impide que el compilador realice inlining, lo que impacta el rendimiento en bucles críticos.
Para que un trait pueda utilizarse como dyn Trait, debe cumplir con las reglas de Object Safety. Si un trait no es “object-safe”, el compilador no puede construir una vtable con un tamaño fijo y predecible. Las reglas principales son:
* No debe tener métodos que retornen Self (ya que el tamaño de Self es desconocido en tiempo de ejecución).
* No debe tener métodos genéricos (la vtable no puede contener infinitas entradas para cada posible combinación de tipos genéricos).
* No debe tener una restricción where Self: Sized a nivel de trait (ya que esto anularía la utilidad de dyn Trait, que se usa precisamente para tipos que no son Sized).
Sin embargo, Rust permite métodos genéricos o con where Self: Sized dentro de un trait object-safe, siempre y cuando dichos métodos se excluyan de la vtable mediante la cláusula where Self: Sized. Estos métodos solo podrán ser invocados mediante el tipo concreto, no a través del puntero dyn Trait.
use std::fmt::Display;
// El trait es "object-safe" porque cumple las reglas.
trait Drawable {
// Método estándar: incluido en la vtable.
fn draw(&self);
// Método con retorno que no es Self: incluido en la vtable.
fn area(&self) -> f64;
// Método genérico: para que el trait sea object-safe,
// debe tener `where Self: Sized`. Esto lo excluye de la vtable.
fn debug_print<T: Display>(&self, label: T) where Self: Sized {
println!("{}: {:?}", label, self);
}
}
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Dibujando un círculo con radio {}", self.radius);
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// Implementación de la versión específica para Self: Sized
fn debug_print<T: Display>(&self, label: T) where Self: Sized {
println!("DEBUG [{}]: Círculo de radio {}", label, self.radius);
}
}
struct Square {
side: f64,
}
impl Drawable for Square {
fn draw(&self) {
println!("Dibujando un cuadrado de lado {}", self.side);
}
fn area(&self) -> f64 {
self.side * self.side
}
fn debug_print<T: Display>(&self, label: T) where Self: Sized {
println!("DEBUG [{}]: Cuadrado de lado {}", label, self.side);
}
}
fn main() {
// Colección heterogénea usando Box<dyn Drawable> (Fat Pointers)
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 4.0 }),
];
for shape in shapes {
// Despacho dinámico: Se accede a la vtable para encontrar draw() y area()
shape.draw();
println!("Área: {}", shape.area());
// ERROR DE COMPILACIÓN: No podemos llamar a debug_print
// a través de 'dyn Drawable' porque es un método genérico/excluido.
// shape.debug_print("Test", "Error");
// Para usarlo, tendríamos que conocer el tipo concreto (no posible en este loop).
}
}
Explicación del Código
trait Drawable: Define la interfaz. Al incluirwhere Self: Sizedendebug_print, permitimos que el trait sea compatible condyn Drawable(object-safe), aunque el métododebug_printno se guardará en la vtable.Box<dyn Drawable>: Crea un fat pointer. En elVec, cada elemento contiene la dirección de la instancia (CircleoSquare) y la dirección de su respectiva vtable.shape.draw(): Es el núcleo del despacho dinámico. El runtime busca en la vtable asociada al punteroshapela dirección del métododrawpara el tipo específico y salta a esa dirección.shape.area(): Similar adraw, utiliza la vtable para resolver la llamada aarea. El tamaño de retornof64es conocido, por lo que es seguro.debug_print: Este método es genérico. Si intentáramos incluirlo en la vtable, la tabla tendría que ser infinita para cubrir todas las combinaciones deT. Por ello, la cláusulawhere Self: Sizedlo marca como un método que solo existe para tipos de tamaño conocido en compilación, permitiendo que el trait sea usable como objeto dinámico para los demás métodos.
N° 105