Elegir el puntero: Box, Rc, Arc y cómo combinarlos
Guía de decisión que cierra el nivel. Toda la elección se reduce a dos preguntas: cuántos dueños y cuántos hilos. Un dueño → `Box`; varios en un hilo → `Rc`; varios en varios hilos → `Arc`. Y una segunda dimensión ortogonal: para mutar lo compartido, compón con `RefCell` (un hilo) o `Mutex` (varios). El mapa que une todo el zoológico de punteros.
Ya tienes las cuatro piezas —Box, Rc, Arc, Weak— más la mutabilidad interior de RefCell y Mutex. El arte no está en cada una por separado, sino en elegir. Y elegir es más fácil de lo que parece, porque casi todo se decide con dos preguntas: ¿cuántos dueños necesita este dato? y ¿cuántos hilos lo tocan?. Un dueño y en la pila: nada, ownership normal. Un dueño en el heap: Box. Varios dueños en un hilo: Rc. Varios dueños en varios hilos: Arc. Y si además hay que mutar lo compartido, se añade una segunda capa. Esta lección es el mapa que une el nivel entero.
- Elegir entre
Box,RcyArcsegún el número de dueños y de hilos. - Combinar la propiedad compartida con la mutabilidad interior:
Rc<RefCell<T>>yArc<Mutex<T>>. - Distinguir la capa de quién posee de la capa de quién puede mutar.
- Reconocer los costes de cada elección y no sobredimensionar.
Dos preguntas, cuatro respuestas
La primera dimensión es la propiedad: cuántos dueños y en cuántos hilos.
flowchart TD
q1{Cuantos duenos} -->|Uno| box_o{En la pila cabe}
box_o -->|Si| pila[Valor normal o referencia]
box_o -->|No: recursivo o dyn o grande| bx[Box de T]
q1 -->|Varios| q2{Cuantos hilos}
q2 -->|Un hilo| rc[Rc de T]
q2 -->|Varios hilos| arc[Arc de T]
style pila fill:#a6e3a1,color:#11111b
style bx fill:#89b4fa,color:#11111b
style rc fill:#cba6f7,color:#11111b
style arc fill:#f9e2af,color:#11111bBox de T
Un dueño, en el heap. Para tipos recursivos, trait objects y valores grandes. Coste cero sobre un puntero crudo.
Rc de T
Varios dueños, un hilo. Conteo de referencias no atómico: barato. clone suma al contador. !Send.
Arc de T
Varios dueños, varios hilos. Conteo atómico: seguro pero más caro. Send + Sync. La base del estado compartido.
Weak de T
Referencia que no mantiene vivo el dato. Rompe ciclos de Rc/Arc. Se usa vía upgrade, que puede dar None.
La otra dimensión: mutabilidad interior
Rc y Arc solo entregan &T: propiedad compartida es acceso compartido, no mutable. Para mutar lo que compartes hay que componer con mutabilidad interior, y aquí manda una segunda pregunta —de nuevo, cuántos hilos—:
Rc<RefCell<T>>: compartido y mutable en un hilo. ElRefCelltraslada las reglas de préstamo a tiempo de ejecución; si las violas, entra enpanic!en vez de no compilar.Arc<Mutex<T>>: compartido y mutable entre varios hilos. ElMutexgarantiza exclusión bloqueando: solo un hilo mantiene el candado a la vez.Arc<RwLock<T>>: variante para “muchos lectores o un escritor”, cuando las lecturas dominan.
El orden importa: es Rc<RefCell<T>>, no RefCell<Rc<T>>. La capa exterior dice cómo se comparte la propiedad; la interior, cómo se obtiene la mutación. Compartes la celda, y cada dueño puede pedirle un préstamo mutable.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let compartido = Rc::new(RefCell::new(vec![1, 2, 3]));
let otro = Rc::clone(&compartido);
otro.borrow_mut().push(4); // muto a traves de un dueno
println!("{:?}", compartido.borrow()); // [1, 2, 3, 4]: ambos lo ven
}
La tabla de decisión completa
Cruzando las dos dimensiones —propiedad y mutabilidad, un hilo o varios— sale el cuadro que resuelve casi cualquier caso:
| Necesito | Un hilo | Varios hilos |
|---|---|---|
| Un dueño, en el heap | Box<T> |
Box<T> (si T: Send) |
| Varios dueños, solo lectura | Rc<T> |
Arc<T> |
| Varios dueños, con mutación | Rc<RefCell<T>> |
Arc<Mutex<T>> |
| Referencia hacia atrás sin poseer | Weak<T> |
Weak<T> (de un Arc) |
Leer el tipo de un dato es, así, leer su contrato: Arc<Mutex<Vec<Cliente>>> te dice sin abrir el código que ese vector es compartido, entre hilos, y mutable bajo candado. La forma anuncia las capacidades.
No sobredimensiones
Cada capa cuesta, y el coste crece por escalones: un valor o una referencia no cuestan nada; Box cuesta una asignación; Rc añade una asignación con contador no atómico; Arc sube a contador atómico; Arc<Mutex<T>> suma además el bloqueo. La regla es alcanzar siempre la herramienta menos potente que resuelva el problema. La mayoría del código no necesita ninguno de estos punteros: le basta con ownership y borrowing. Recurre a Rc<RefCell<T>> para esquivar al borrow checker cuando el diseño lo pide de verdad —un grafo, un observador, un árbol con vuelta atrás—, no como atajo para no pensar la propiedad; ese abuso es un olor a código reconocido, “pelear con el borrow checker a base de comprobaciones en tiempo de ejecución”.
Ante la duda, empieza por un valor poseído y referencias prestadas. Si el compilador te fuerza a compartir propiedad, sube a Rc. Si de verdad cruzas hilos, sube a Arc. Si necesitas mutar lo compartido, añade RefCell o Mutex dentro. Cada peldaño se justifica por una necesidad concreta que el anterior no cubría; subirlos “por si acaso” solo compra coste y complejidad sin comprar nada a cambio.
Visto desde fuera, tener Box, Rc, Arc, Weak, RefCell y Mutex como piezas separadas parece complejidad gratuita: otros lenguajes se apañan con una sola clase de referencia. Pero esa aparente simplicidad esconde un agrupamiento silencioso de costes. En un lenguaje con recolector de basura, cada objeto está en el heap, es compartible por referencia y es mutable por defecto: pagas asignación, indirección y sincronización potencial siempre, y —peor— no puedes razonar con precisión sobre ninguna de las tres, porque el lenguaje te las dio todas juntas sin preguntarte. Rust hace la operación inversa: descompone esas propiedades fundidas en ejes ortogonales, y te da un tipo para optar a cada uno por separado. Colocación —pila o Box—. Cardinalidad de la propiedad —único, o Rc, o Arc—. Alcance entre hilos —Rc se queda, Arc viaja—. Mutabilidad —inmutable, o RefCell, o Mutex—. Y como son ortogonales, se componen: Rc<RefCell<T>> no es una fórmula que memorizas, es una frase que afirma, en el tipo, exactamente qué capacidades tiene ese dato —compartido, en un hilo, mutable con comprobación en ejecución— y ni una más. Quien lee Arc<Mutex<T>> conoce el contrato sin leer una línea de lógica; quien lee Box<T> sabe que hay un solo dueño y ningún coste de conteo. Esta es la filosofía entera del nivel dicha de una vez: paga exactamente por las capacidades que nombras, nombra exactamente las que usas, y deja que el compilador demuestre que nunca te excediste. El zoológico de punteros no es barroquismo; es la disciplina de no cobrarte por lo que no pediste, convertida en un catálogo de tipos que se combinan. Cuando eliges entre Box y Rc, o entre Rc y Arc, no estás sorteando trámites del lenguaje: estás declarando la verdad sobre tu dato —cuántos lo poseen, cuántos hilos lo alcanzan, si cambia— y esa declaración, verificada en compilación, es a la vez tu documentación, tu garantía de rendimiento y tu prueba de corrección.
Dos preguntas fijan la capa de propiedad: cuántos dueños y cuántos hilos. Un dueño en el heap, Box<T>; varios dueños en un hilo, Rc<T>; varios dueños entre hilos, Arc<T>; y Weak<T> para referencias que no deben mantener vivo el dato. Una segunda capa, ortogonal, aporta mutación a lo compartido: RefCell en un hilo, Mutex o RwLock entre varios, siempre por dentro —Rc<RefCell<T>>, Arc<Mutex<T>>—. Escala por coste y no subas de peldaño sin una necesidad concreta: el tipo que eliges es, a la vez, el contrato del dato, su garantía de rendimiento y su prueba de corrección.
- Para cada caso, di qué puntero usarías y por qué: un árbol de sintaxis que se recorre y no se comparte; una tabla de configuración leída por diez componentes en un hilo; un contador incrementado por ocho hilos; un caché de solo lectura compartido entre hilos.
- Construye un
Rc<RefCell<Vec<i32>>>, clónalo, muta el vector desde un clon y comprueba que el otro ve el cambio. Explica qué hace cada capa. - Explica por qué es
Rc<RefCell<T>>y noRefCell<Rc<T>>. ¿Qué comparte cada disposición y qué se puede mutar en cada una? - Toma un
Rc<RefCell<T>>que funcione en un hilo y conviértelo a su equivalente multihilo. Nombra las dos sustituciones y por qué cada una es necesaria. - Ordena de menor a mayor coste:
&T,Arc<Mutex<T>>,Box<T>,Rc<T>,Arc<T>. Justifica el orden y da una regla para no sobredimensionar.