En Rust, la polimorfismo puede lograrse de dos maneras: mediante despacho estático o despacho dinámico. El despacho estático, común al usar impl Trait o genéricos, ocurre en tiempo de compilación mediante la monomorfización, lo que genera una versión específica del código para cada tipo, permitiendo al compilador realizar inlining y optimizaciones agresivas. Sin embargo, esto no permite tratar diferentes tipos como si fueran el mismo en una colección, como un Vec de diferentes estructuras.
Para resolver esto, Rust utiliza los Trait Objects mediante la palabra clave dyn Trait. Un trait object permite la existencia de polimorfismo en tiempo de ejecución. Como los tipos que implementan un trait pueden tener tamaños diferentes, dyn Trait es un tipo de tamaño desconocido en tiempo de compilación (unsized). Por ello, siempre debemos referenciarlo a través de un puntero, siendo el más común Box<dyn Trait>.
El mecanismo detrás de esto es la vtable (tabla de métodos virtuales). Un puntero a un trait object es en realidad un “puntero grueso” (fat pointer) que contiene dos elementos: un puntero a la instancia de datos en la memoria y un puntero a una vtable. La vtable es una estructura de datos que contiene los endereces de las implementaciones de los métodos de ese trait para ese tipo específico. Al llamar a un método, el programa busca la dirección del método en la vtable y salta a ella. Esto introduce un pequeño costo de rendimiento debido a la indirección de memoria y la imposibilidad de realizar inlining.
Para que un trait pueda convertirse en un trait object, debe cumplir con las reglas de seguridad de objetos (object safety). Las reglas principales dictan que el trait no puede tener métodos que retornen Self (ya que el compilador no sabría qué tamaño reservar para el retorno) ni métodos que utilicen parámetros genéricos (ya que esto requeriría una vtable infinita para cada posible tipo genérico).
// Definición del trait que será usado como Trait Object
trait Dibujar {
// Método seguro para objetos: retorna algo que no es Self
fn dibujar(&self);
// Método que retorna un valor fijo para ser seguro
fn area(&self) -> f64;
}
struct Circulo {
radio: f64,
}
struct Cuadrado {
lado: f64,
}
impl Dibujar for Circulo {
fn dibujar(&self) {
println!("Dibujando un círculo con radio: {}", self.radio);
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radio * self.radio
}
}
impl Dibujar for Cuadrado {
fn dibujar(&self) {
println!("Dibujando un cuadrado con lado: {}", self.lado);
}
fn area(&self) -> f64 {
self.lado * self.lado
}
}
fn main() {
// Creamos una colección heterogénea usando Box<dyn Dibujar>
// Esto permite mezclar diferentes tipos que implementan el mismo trait
let mut formas: Vec<Box<dyn Dibujar>> = Vec::new();
formas.push(Box::new(Circulo { radio: 5.0 }));
formas.push(Box::new(Cuadrado { lado: 4.0 }));
// Despacho dinámico: el tipo exacto se conoce solo en runtime
// a través de la vtable de cada objeto en la colección.
for forma in formas.iter() {
forma.dibujar();
println!("Área: {:.2}", forma.area());
}
}
Explicación del Código
trait Dibujar: Define la interfaz común. Es “object safe” porquedibujarno retornaSelfyarearetorna unf64(un tipo con tamaño conocido), permitiendo que el compilador gestione la pila adecuadamente.struct Circuloystruct Cuadrado: Son los tipos concretos. Tienen tamaños distintos en memoria.impl Dibujar for ...: Implementan la lógica específica para cada tipo.Vec<Box<dyn Dibujar>>: Este es el punto clave. ElVecno puede almacenardyn Dibujardirectamente porque no sabe cuánto espacio reservar por cada elemento. Al usarBox<dyn Dibujar>, elVecalmacena punteros de tamaño fijo. CadaBoxapunta a la instancia en el heap y contiene la información de la vtable necesaria para identificar si se trata de unCirculoo unCuadrado.forma.dibujar(): Durante la iteración, cuando llamamos adibujar, el programa no sabe en tiempo de compilación siformaes un círculo o un cuadrado. En su lugar, sigue el puntero a la vtable contenido en el fat pointer para encontrar la dirección de la implementación correcta dedibujarpara ese objeto específico y ejecuta el código correspondiente.
N° 45