Punteros inteligentes en Rust
Las reglas de ownership (propiedad) y borrowing (préstamo) cubren casi todas las situaciones, pero hay algunos casos reales que no encajan del todo en “exactamente un dueño”: un valor cuyo tamaño no se conoce hasta que el programa se ejecuta, datos que de verdad necesitan más de un dueño, o un valor que tienes que modificar a través de una referencia compartida. Los smart pointers (punteros inteligentes) son la respuesta de Rust a cada uno de esos casos.
Box<T>: guardar un valor en el heap
Sección titulada «Box<T>: guardar un valor en el heap»Por defecto, los valores viven en la pila (stack), y el compilador necesita saber su tamaño de antemano. Box<T> mueve un valor al heap (el montón) y te da un puntero de tamaño fijo hacia él. Es útil cuando el tamaño de un tipo no se puede conocer al compilar, sobre todo con tipos recursivos:
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * para sacar el valor}Salida:
5Un ejemplo clásico de cuándo necesitas Box: un tipo que se contiene a sí mismo, como el nodo de una lista enlazada:
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("Primer valor: {value}"), List::Nil => println!("Vacía"), }}Salida:
Primer valor: 1Sin Box, Cons(i32, List) necesitaría un espacio infinito: una List que contiene una List que contiene una List, para siempre. En cambio, Box<List> es un puntero de tamaño fijo, así que el compilador puede calcular cuánto espacio hace falta.
Rc<T>: varios dueños
Sección titulada «Rc<T>: varios dueños»El ownership dice que un valor tiene exactamente un dueño. Casi siempre eso es justo lo que quieres, pero a veces de verdad necesitas que varias partes de tu programa compartan los mismos datos. Rc<T> (“reference counted”, con conteo de referencias) lo permite, llevando la cuenta de cuántos dueños existen en cada momento:
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("datos compartidos")); let b = Rc::clone(&a); // no es una copia completa: solo otro dueño más let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("Dueños: {}", Rc::strong_count(&a));}Salida:
datos compartidos datos compartidos datos compartidosDueños: 3Rc::clone no copia los datos de dentro: solo aumenta un contador y te devuelve otra referencia al mismo valor. Los datos se liberan cuando todos sus dueños han salido de su ámbito (scope).
RefCell<T>: modificar a través de una referencia compartida
Sección titulada «RefCell<T>: modificar a través de una referencia compartida»Normalmente, las reglas de borrowing se comprueban al compilar: o muchas referencias de solo lectura, o una mutable, nunca las dos cosas a la vez. RefCell<T> traslada esa misma comprobación al momento de ejecución, lo que te permite modificar un valor incluso a través de lo que parece una referencia compartida y sin mut:
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}Salida:
6RefCell suele ir de la mano de Rc: Rc<RefCell<T>> te da un valor con varios dueños que además se puede modificar, algo que Rc<T> por sí solo no permite.
Cuándo usarlos de verdad
Sección titulada «Cuándo usarlos de verdad»No empieces por los smart pointers. Empieza con ownership y borrowing normales, y recurre a ellos solo cuando el compilador te esté diciendo claramente que no encaja: un tipo recursivo (Box), un valor que de verdad necesita varios dueños (Rc) o una modificación a través de una referencia compartida que las reglas de borrowing no permiten (RefCell).
Práctica
Sección titulada «Práctica»1. Suma una lista enlazada
Sección titulada «1. Suma una lista enlazada»Usa el enum List de la sección de Box. Construye la lista 1, 2, 3 y escribe una función fn sum(list: &List) -> i32 que sume todos los valores. Pista: usa match sobre la lista. Para Cons(value, rest), la respuesta es value más la suma de rest. Para Nil, es 0.
Ver solución
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("Suma: {}", sum(&list));}Salida:
Suma: 6La función se llama a sí misma con el resto de la lista hasta llegar a Nil. Fíjate en que puedes pasar rest (un &Box<List>) donde se espera un &List: Rust mira a través del Box por ti.
2. Una lista de la compra compartida
Sección titulada «2. Una lista de la compra compartida»Crea una lista de la compra (un Vec<String>) que dos personas, alice y bob, posean a la vez y a la que ambas puedan añadir cosas. Cada una añade un artículo. Después imprime la lista y el número de dueños. Pista: envuelve el Vec en Rc<RefCell<...>>, dale a cada persona un Rc::clone y llama a .borrow_mut().push(...) para añadir.
Ver solución
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("leche")); bob.borrow_mut().push(String::from("pan"));
println!("{:?}", list.borrow()); println!("Dueños: {}", Rc::strong_count(&list));}Salida:
["leche", "pan"]Dueños: 3Hay tres dueños: list, alice y bob. Los tres apuntan al mismo Vec, así que un artículo añadido a través de alice también se ve a través de bob y de list. Ninguna de las variables necesita mut, porque RefCell se encarga de la modificación.
Resumen
Sección titulada «Resumen»Box<T>guarda un valor en el heap con un puntero de tamaño fijo; es necesario para los tipos recursivos.Rc<T>permite que un mismo valor tenga varios dueños en código de un solo hilo, gracias al conteo de referencias; entre hilos, usaArc<T>.RefCell<T>comprueba las reglas de borrowing al ejecutar en lugar de al compilar, lo que permite modificar a través de una referencia compartida, pero provoca un panic si se rompen las reglas.- Recurre a ellos solo cuando el ownership y el borrowing normales no encajen con tu caso.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Por qué un tipo recursivo, como el nodo de una lista enlazada, necesita Box<T>?
2. ¿Qué hace realmente Rc::clone(&a)?
3. ¿Cuándo comprueba RefCell<T> las reglas de borrowing?
4. ¿Qué te da Rc<RefCell<T>> que Rc<T> por sí solo no te da?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Usar
Box<T>para apuntar a datos en el heap — El libro de Rust (en inglés): la historia completa deBox, incluido el ejemplo de la lista cons que usamos en esta página. Rc<T>, el smart pointer con conteo de referencias — El libro de Rust (en inglés): el ownership compartido paso a paso, viendo cómo el contador sube y baja.RefCell<T>y el patrón de mutabilidad interior — El libro de Rust (en inglés): cuándo y por qué trasladar las comprobaciones de préstamos al momento de ejecución, con un ejemplo realista.std::cell— documentación de la biblioteca estándar (en inglés): explicaCellyRefCell, y cuándo elegir cada uno.- Rc — Rust by Example (en inglés): un ejemplo corto y ejecutable que imprime el contador de referencias a medida que aparecen y desaparecen dueños.