Box en Rust: heap allocation y tipos recursivos

Box<T> es el puntero inteligente más simple de Rust: asigna un valor en el heap y guarda un puntero de tamaño fijo en el stack. Cuando el Box sale de scope, tanto el puntero como el dato en el heap se destruyen automáticamente. Lo usarás principalmente en tres situaciones: tipos recursivos, dispatch dinámico y transfer de ownership de datos grandes sin copiarlos.

Sintaxis básica

fn main() {
    let b = Box::new(5); // 5 está en el heap, b en el stack
    println!("{}", b);   // se desreferencia automáticamente
    println!("{}", *b);  // desreferencia explícita también funciona
} // b sale de scope ? se libera el heap automáticamente

Por qué Box: los tres casos de uso

1. Tipos recursivos (lista enlazada)

// Sin Box: ERROR de compilación
// enum Lista {
//     Cons(i32, Lista), // tamaño infinito, el compilador no puede calcularlo
//     Nil,
// }

// Con Box: OK
enum Lista {
    Cons(i32, Box<Lista>), // Box tiene tamaño fijo (un puntero)
    Nil,
}

fn main() {
    let lista = Lista::Cons(1,
        Box::new(Lista::Cons(2,
            Box::new(Lista::Cons(3,
                Box::new(Lista::Nil))))));
}

El compilador necesita conocer el tamaño de cada tipo en compilación. Un tipo recursivo sin Box tendría tamaño infinito. Con Box, el tamaño es siempre el de un puntero (8 bytes en 64 bits).

2. Dispatch dinámico con Box<dyn Trait>

trait Animal {
    fn hablar(&self) -> &str;
}

struct Perro;
struct Gato;

impl Animal for Perro {
    fn hablar(&self) -> &str { "Guau" }
}

impl Animal for Gato {
    fn hablar(&self) -> &str { "Miau" }
}

fn main() {
    let animales: Vec<Box<dyn Animal>> = vec![
        Box::new(Perro),
        Box::new(Gato),
        Box::new(Perro),
    ];

    for animal in &animales {
        println!("{}", animal.hablar());
    }
}

Box<dyn Trait> usa dispatch dinámico (vtable): la llamada se resuelve en runtime en lugar de en compilación. Útil cuando necesitas colecciones de tipos heterogéneos que comparten un trait.

3. Manejo de errores heterogéneos

fn puede_fallar() -> Result<String, Box<dyn std::error::Error>> {
    let contenido = std::fs::read_to_string("config.txt")?;
    let n: i32 = contenido.trim().parse()?;
    Ok(format!("Número: {}", n))
}

Box<dyn Error> acepta cualquier error: io::Error, ParseIntError, etc. No necesitas definir un enum de errores.

Dispatch estático vs dinámico

Aspectoimpl Trait / T: TraitBox<dyn Trait>
ResoluciónCompilación (monomorfización)Runtime (vtable)
RendimientoMáximo (inlining posible)Pequeño overhead por vtable
Colecciones heterogéneasNo
Tamaño del binarioMayor (una copia por tipo)Menor

Deref coercion

fn imprimir(s: &str) {
    println!("{}", s);
}

fn main() {
    let s = Box::new(String::from("hola"));
    imprimir(&s); // Box<String> ? &String ? &str (deref coercion)
}

Resumen

  • Box<T>: asigna T en el heap con propiedad única. Se destruye automáticamente.
  • Tipos recursivos: usa Box para romper el ciclo de tamaño infinito.
  • Box<dyn Trait>: dispatch dinámico para colecciones heterogéneas.
  • Box<dyn Error>: tipo de error comodín en aplicaciones.
  • Deref coercion: &Box<T> se convierte automáticamente a &T.

El siguiente artículo cubre Rc<T> y Arc<T>: cómo tener múltiples propietarios del mismo dato mediante reference counting.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP