Ir al contenido

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.

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:

5
▶ Pruébalo tú mismo

Un 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: 1
▶ Pruébalo tú mismo

Sin 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.

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 compartidos
Dueños: 3
▶ Pruébalo tú mismo

Rc::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:

6
▶ Pruébalo tú mismo

RefCell 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.

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).

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: 6
▶ Pruébalo tú mismo

La 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.

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: 3
▶ Pruébalo tú mismo

Hay 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.

  • 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, usa Arc<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 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