Punteros inteligentes: Box, Rc y RefCell
Las referencias comunes solo prestan; los punteros inteligentes poseen, cuentan referencias y a veces trasladan el chequeo de préstamos al tiempo de ejecución, permitiendo formas que los tipos por sí solos no pueden describir.
Todos los valores con los que has trabajado hasta ahora siguen la regla central de ownership de Rust: un solo dueño a la vez, y el valor se libera en el instante en que ese dueño sale de su alcance (scope). Esa única regla es lo que le permite a Rust garantizar seguridad de memoria sin un recolector de basura, pero también es, por diseño, restrictiva. No te dice qué hacer cuando un valor realmente necesita más de un dueño, cuando su tamaño no se puede conocer en tiempo de compilación, o cuando necesitas mutar algo a través de lo que el borrow checker considera una referencia inmutable. Los punteros inteligentes existen precisamente para cubrir esos tres huecos, y cada uno que verás en esta lección — Box, Rc, RefCell — relaja una regla distinta en lugar de romperla.
Un puntero inteligente, a diferencia de una referencia común, es un struct que posee (owns) el dato al que apunta y que normalmente implementa dos traits: `Deref`, para comportarse como una referencia cuando lo desreferencias con `*`, y `Drop`, para ejecutar lógica de limpieza en el instante en que sale de scope. No hay magia del compilador escondida detrás: podrías escribir tu propio tipo de puntero inteligente usando esos mismos dos traits. Vale la pena interiorizar esto antes de ver el código, porque significa que Box, Rc y RefCell no son excepciones al modelo de ownership de Rust; están construidos completamente sobre él.
`Box<T>` es el más simple: coloca un valor en el heap en lugar del stack, pero sigue exigiendo un único dueño y verificación de préstamos en tiempo de compilación exactamente igual que un valor en el stack. Lo usas en dos situaciones. Primero, los tipos recursivos — un struct que se contiene a sí mismo, como un nodo de una lista enlazada o un árbol — no pueden tener un tamaño que el compilador pueda calcular, porque recursaría infinitamente; envolver el campo recursivo en un `Box` le da un tamaño fijo (un puntero siempre ocupa la misma cantidad de bytes) y rompe la recursión infinita. Segundo, los trait objects: cuando necesitas que un `Vec` o un campo contenga 'cualquier cosa que implemente este trait' en lugar de un único tipo concreto, guardas `Box<dyn Trait>`, porque el compilador no puede calcular el tamaño de un tipo concreto desconocido, pero siempre puede calcular el tamaño de un puntero hacia él.
`Rc<T>` — 'reference counted', contado por referencias — resuelve el problema opuesto: ownership compartido en código de un solo hilo. Llamar a `.clone()` sobre un `Rc` no copia el dato subyacente; incrementa un contador interno y devuelve otro puntero a la misma reserva del heap. El valor solo se libera cuando ese contador llega a cero, lo que significa que el último dueño en salir de scope es quien realmente lo libera. La contrapartida es que `Rc` solo entrega acceso inmutable — su `Deref` te da un `&T`, nunca un `&mut T` — porque si dos dueños pudieran mutar el mismo dato al mismo tiempo, la garantía de aliasing de Rust (muchos lectores o un solo escritor, nunca ambos) se vendría abajo.
Ese es exactamente el hueco que llena `RefCell<T>`. Exige las mismas reglas de préstamo de Rust — como máximo un préstamo mutable, o cualquier cantidad de préstamos inmutables, nunca ambos a la vez — pero las verifica en tiempo de ejecución en lugar de en tiempo de compilación. Llama a `.borrow()` para una vista de solo lectura o a `.borrow_mut()` para una mutable; cada una devuelve un guard inteligente, y si llegas a mantener un `borrow_mut()` mientras otro préstamo sigue vivo, el programa compila perfectamente y luego entra en panic en cuanto se ejecuta. Suena a un retroceso, y en cierto sentido lo es — estás cambiando un error de compilación por uno de ejecución — pero es la única forma de mutar algo que el compilador, por razones estructurales, ha decidido que debe ser inmutable.
Combina `Rc` y `RefCell` — `Rc<RefCell<T>>` — y obtienes exactamente lo que ninguno ofrece por separado: un valor con múltiples dueños, cualquiera de los cuales puede mutarlo. Este es el patrón estándar para estado compartido y mutable en Rust de un solo hilo — piensa en un grafo donde varios nodos necesitan actualizar un contador compartido, o un árbol de widgets de interfaz donde un padre y un hijo necesitan acceso de escritura al mismo estado. No es gratis: cada mutación ahora conlleva una verificación de préstamo en tiempo de ejecución, y es totalmente posible escribir código que compile perfectamente y luego entre en panic en producción la primera vez que dos préstamos se solapen. Usado con moderación es una herramienta precisa; usado en todas partes es señal de que estás peleando contra el modelo de ownership en lugar de diseñar con él — y vale la pena saber que `Weak<T>`, una versión de `Rc` que no posee el valor, existe específicamente para romper los ciclos de referencias que este patrón puede crear por accidente.
// A classic recursive type: a singly linked list built from an enum.// Without Box, Rust cannot compute the size of List (it would contain itself).enum List {Cons(i32, Box<List>),Nil,}use List::{Cons, Nil};fn sum(list: &List) -> i32 {match list {Cons(value, rest) => value + sum(rest),Nil => 0,}}fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));println!("sum = {}", sum(&list));}
A recursive enum only compiles once the recursive branch is boxed — the Box gives the compiler a fixed-size pointer to work with instead of an infinitely nested type.
use std::rc::Rc;fn main() {let owner_a = Rc::new(String::from("shared config"));println!("count after creating owner_a: {}", Rc::strong_count(&owner_a));let owner_b = Rc::clone(&owner_a);println!("count after cloning into owner_b: {}", Rc::strong_count(&owner_a));{let owner_c = Rc::clone(&owner_a);println!("count with owner_c alive: {}", Rc::strong_count(&owner_a));println!("owner_c sees: {owner_c}");}println!("count after owner_c dropped: {}", Rc::strong_count(&owner_a));println!("owner_a and owner_b still valid: {owner_a}, {owner_b}");}
Cloning an Rc never copies the data — it just increments the reference count, so every clone is a cheap, shared view of the same heap allocation.
use std::cell::RefCell;use std::rc::Rc;struct SharedCounter {value: i32,}fn main() {let counter = Rc::new(RefCell::new(SharedCounter { value: 0 }));let handle_a = Rc::clone(&counter);let handle_b = Rc::clone(&counter);handle_a.borrow_mut().value += 5;handle_b.borrow_mut().value += 10;println!("final value: {}", counter.borrow().value);println!("total owners: {}", Rc::strong_count(&counter));}
handle_a and handle_b are separate Rc pointers to the same RefCell, so each borrow_mut() call mutates the one shared SharedCounter — this is the pattern to reach for when several owners must all be able to write.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. You need a `Vec<Node>` where `Node` is a struct containing a field of type `Rc<RefCell<Node>>` pointing to its parent, and multiple children need to mutate their shared parent's data. Why is `Rc<T>` alone not enough here, even though `Rc` supports multiple owners?