19 - Smart pointers en Rust Box, Rc y RefCell

19 - Smart pointers en Rust Box, Rc y RefCell

¡Bienvenidos de nuevo a Rustaceo! En nuestras aventuras anteriores, hemos explorado muchos conceptos fundamentales de Rust, como propiedad, préstamos, genéricos, traits y tiempos de vida. Hoy nos adentraremos en un tema esencial para manejar estructuras de datos complejas y referencias en Rust: los Smart Pointers o Punteros Inteligentes. Nos enfocaremos en Box<T>, Rc<T> y RefCell<T>, y cómo utilizarlos correctamente en nuestros programas. Como siempre, utilizaremos ejemplos del mundo Pokémon para hacer este viaje más entretenido y comprensible.

¿Qué son los smart pointers? #

En Rust, un Smart Pointer es una estructura que no solo actúa como un puntero, sino que también posee metadatos adicionales y capacidades. A diferencia de los punteros regulares, los Smart Pointers en Rust implementan el trait Deref y, a veces, Drop, permitiendo comportamientos similares a los punteros y automatizando la liberación de recursos.

Los Smart Pointers más comunes en Rust son:

  • Box<T>: Para almacenar datos en el heap.
  • Rc<T>: Contador de referencias para datos compartidos en un solo hilo.
  • RefCell<T>: Permite mutabilidad interior y chequeos de préstamos en tiempo de ejecución.

Box<t>: almacenando datos en el heap #

¿Cuándo usar box<t>? #

  • Tamaño Desconocido en Tiempo de Compilación: Al trabajar con tipos recursivos, como listas enlazadas o árboles.
  • Mover Datos al Heap: Cuando necesitamos que un dato viva en el heap en lugar de en la pila.
  • Implementar Traits sin Parámetros de Tipo: Cuando necesitamos un tipo concreto que implemente un trait.

Ejemplo: lista enlazada simple #

Supongamos que queremos implementar una lista enlazada de Pokémon.

use std::fmt::Display;

struct Pokémon {
    nombre: String,
    tipo: String,
}

impl Display for Pokémon {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} ({})", self.nombre, self.tipo)
    }
}

enum ListaEnlazada {
    Nodo(Pokémon, Box<ListaEnlazada>),
    Vacio,
}

use ListaEnlazada::{Nodo, Vacio};

fn main() {
    let item1 = Pokémon {
        nombre: String::from("Item1"),
        tipo: String::from("Eléctrico"),
    };

    let item4 = Pokémon {
        nombre: String::from("Item4"),
        tipo: String::from("Planta"),
    };

    let lista = Nodo(
        item1,
        Box::new(Nodo(
            item4,
            Box::new(Vacio),
        )),
    );

    imprimir_lista(&lista);
}

fn imprimir_lista(lista: &ListaEnlazada) {
    match lista {
        Nodo(pokemon, siguiente) => {
            println!("{}", pokemon);
            imprimir_lista(siguiente);
        }
        Vacio => (),
    }
}

Explicación:

  • Box<T>: Nos permite tener un tamaño conocido en tiempo de compilación para ListaEnlazada.
  • Recursividad: Sin Box<T>, el enum tendría un tamaño infinito debido a la recursividad.

Más información #

Rc<t>: contador de referencias para datos compartidos #

¿Cuándo usar rc<t>? #

  • Datos Compartidos entre Múltiples Propietarios: Cuando necesitamos que múltiples partes de nuestro programa posean el mismo dato.
  • Inmutabilidad Compartida: Los datos a los que apunta Rc<T> son inmutables por defecto.

Ejemplo: árbol de evoluciones pokémon #

Supongamos que queremos representar las evoluciones de un Pokémon, donde un Pokémon puede evolucionar a varios otros.

use std::rc::Rc;

struct Pokémon {
    nombre: String,
    evoluciones: Vec<Rc<Pokémon>>,
}

fn main() {
    let eevee = Rc::new(Pokémon {
        nombre: String::from("Eevee"),
        evoluciones: Vec::new(),
    });

    let vaporeon = Rc::new(Pokémon {
        nombre: String::from("Vaporeon"),
        evoluciones: Vec::new(),
    });

    let jolteon = Rc::new(Pokémon {
        nombre: String::from("Jolteon"),
        evoluciones: Vec::new(),
    });

    let flareon = Rc::new(Pokémon {
        nombre: String::from("Flareon"),
        evoluciones: Vec::new(),
    });

    // Agregamos las evoluciones a Eevee
    let mut eevee_mut = Rc::get_mut(&mut Rc::clone(&eevee)).unwrap();
    eevee_mut.evoluciones.push(Rc::clone(&vaporeon));
    eevee_mut.evoluciones.push(Rc::clone(&jolteon));
    eevee_mut.evoluciones.push(Rc::clone(&flareon));

    imprimir_evoluciones(&eevee);
}

fn imprimir_evoluciones(pokemon: &Rc<Pokémon>) {
    println!("Evoluciones de {}:", pokemon.nombre);
    for evolucion in &pokemon.evoluciones {
        println!("- {}", evolucion.nombre);
    }
}

Explicación:

  • Rc<T>: Permite múltiples propietarios de los datos, en este caso, las evoluciones.
  • Clonación de Rc<T>: Al clonar un Rc<T>, incrementamos su contador de referencias.

Nota Importante: Rc<T> no es seguro para usar en contextos de múltiples hilos. Para ello, debemos utilizar Arc<T>.

Más información #

Refcell<t>: mutabilidad interior #

¿Cuándo usar refcell<t>? #

  • Mutabilidad en Datos Inmutables: Cuando necesitamos modificar datos aunque tengamos referencias inmutables.
  • Chequeos de Préstamos en Tiempo de Ejecución: RefCell<T> realiza chequeos de préstamos en tiempo de ejecución en lugar de en tiempo de compilación.

Ejemplo: registro de batallas pokémon #

Supongamos que queremos mantener un registro de batallas y actualizar el conteo de victorias y derrotas de cada Pokémon.

use std::cell::RefCell;
use std::rc::Rc;

struct Pokémon {
    nombre: String,
    victorias: RefCell<u32>,
    derrotas: RefCell<u32>,
}

fn main() {
    let item1 = Rc::new(Pokémon {
        nombre: String::from("Item1"),
        victorias: RefCell::new(0),
        derrotas: RefCell::new(0),
    });

    let item2 = Rc::new(Pokémon {
        nombre: String::from("Item2"),
        victorias: RefCell::new(0),
        derrotas: RefCell::new(0),
    });

    registrar_batalla(&item1, &item2, "Item1");

    println!(
        "{} tiene {} victorias y {} derrotas.",
        item1.nombre,
        item1.victorias.borrow(),
        item1.derrotas.borrow()
    );

    println!(
        "{} tiene {} victorias y {} derrotas.",
        item2.nombre,
        item2.victorias.borrow(),
        item2.derrotas.borrow()
    );
}

fn registrar_batalla(
    elemento1: &Rc<Pokémon>,
    elemento2: &Rc<Pokémon>,
    ganador: &str,
) {
    if elemento1.nombre == ganador {
        *elemento1.victorias.borrow_mut() += 1;
        *elemento2.derrotas.borrow_mut() += 1;
    } else if elemento2.nombre == ganador {
        *elemento2.victorias.borrow_mut() += 1;
        *elemento1.derrotas.borrow_mut() += 1;
    }
}

Explicación:

  • RefCell<T>: Permite mutar datos aunque tengamos una referencia inmutable al Pokémon.
  • borrow_mut y borrow: Métodos para obtener referencias mutables e inmutables en tiempo de ejecución.
  • Combinar Rc<T> y RefCell<T>: Nos permite tener múltiples propietarios y mutabilidad interior.

Más información #

Combinando rc<t> y refcell<t> #

Es común combinar Rc<T> y RefCell<T> para tener múltiples propietarios de datos que pueden mutarse.

Ejemplo: grupos de entrenamiento pokémon #

Imaginemos que tenemos varios entrenadores que comparten Pokémon para entrenar.

use std::cell::RefCell;
use std::rc::Rc;

struct Pokémon {
    nombre: String,
    nivel: RefCell<u8>,
}

struct Entrenador {
    nombre: String,
    pokemon: Vec<Rc<Pokémon>>,
}

fn main() {
    let item1 = Rc::new(Pokémon {
        nombre: String::from("Item1"),
        nivel: RefCell::new(25),
    });

    let ash = Entrenador {
        nombre: String::from("Ash"),
        pokemon: vec![Rc::clone(&item1)],
    };

    let misty = Entrenador {
        nombre: String::from("Misty"),
        pokemon: vec![Rc::clone(&item1)],
    };

    // Ambos entrenadores entrenan al mismo Item1
    entrenar_elemento(&ash.pokemon);
    entrenar_elemento(&misty.pokemon);

    println!(
        "{} ahora está en el nivel {}.",
        item1.nombre,
        item1.nivel.borrow()
    );
}

fn entrenar_elemento(elemento_list: &Vec<Rc<Pokémon>>) {
    for pokemon in elemento_list {
        *pokemon.nivel.borrow_mut() += 1;
    }
}

Salida:

Item1 ahora está en el nivel 27.

Explicación:

  • Tanto Ash como Misty tienen una referencia al mismo Item1.
  • Utilizamos Rc<Pokémon> para permitir múltiples propietarios.
  • RefCell<u8> permite mutar el nivel del Pokémon incluso a través de referencias inmutables.

Advertencias y consideraciones #

Chequeos en tiempo de ejecución #

  • RefCell<T> realiza chequeos de préstamos en tiempo de ejecución.
  • Si intentamos obtener más de una referencia mutable o una referencia mutable mientras haya referencias inmutables activas, el programa entrará en pánico en tiempo de ejecución.

Ciclos de referencias #

  • Rc<T> puede provocar ciclos de referencias que impiden que los datos sean liberados.
  • Para evitar esto, podemos usar Weak<T>, una referencia débil que no incrementa el contador de referencias.

Uso en hilos múltiples #

  • Rc<T> no es seguro para usar en contextos de múltiples hilos.
  • Para referencias compartidas entre hilos, debemos utilizar Arc<T>.

Ejemplo avanzado: gráficos de pokémon #

Supongamos que queremos modelar un gráfico donde los nodos son Pokémon y las aristas representan amistades.

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Pokémon {
    nombre: String,
    amigos: RefCell<Vec<Weak<Pokémon>>>,
}

fn main() {
    let item1 = Rc::new(Pokémon {
        nombre: String::from("Item1"),
        amigos: RefCell::new(Vec::new()),
    });

    let item4 = Rc::new(Pokémon {
        nombre: String::from("Item4"),
        amigos: RefCell::new(Vec::new()),
    });

    item1.amigos.borrow_mut().push(Rc::downgrade(&item4));
    item4.amigos.borrow_mut().push(Rc::downgrade(&item1));

    imprimir_amigos(&item1);
    imprimir_amigos(&item4);
}

fn imprimir_amigos(pokemon: &Rc<Pokémon>) {
    println!("Amigos de {}:", pokemon.nombre);
    for amigo_weak in pokemon.amigos.borrow().iter() {
        if let Some(amigo) = amigo_weak.upgrade() {
            println!("- {}", amigo.nombre);
        }
    }
}

Explicación:

  • Weak<T>: Una referencia débil que no incrementa el contador de referencias.
  • Evitar Ciclos: Al usar Weak<T>, evitamos ciclos de referencias que impedirían que los datos sean liberados.
  • upgrade: Convierte un Weak<T> en un Option<Rc<T>> si la referencia aún es válida.

Más información #

Conclusión #

Los Smart Pointers en Rust son herramientas poderosas que nos permiten manejar estructuras de datos complejas y referencias de manera segura y eficiente. Al entender cuándo y cómo utilizar Box<T>, Rc<T>, RefCell<T> y Weak<T>, podemos escribir código Rust más robusto y flexible.

Resumen:

  • Box<T>: Para datos en el heap y tipos recursivos.
  • Rc<T>: Para compartir datos inmutables entre múltiples propietarios.
  • RefCell<T>: Para mutabilidad interior con chequeos en tiempo de ejecución.
  • Combinaciones: Usar Rc<T> y RefCell<T> juntos para datos compartidos y mutables.
  • Weak<T>: Para evitar ciclos de referencias con Rc<T>.

¿Te ha resultado útil este artículo? Te animo a que experimentes implementando tus propias estructuras y manejando referencias utilizando Smart Pointers. Con práctica, podrás aprovechar al máximo las capacidades de Rust para manejar memoria y referencias de manera segura.

Si tienes preguntas o quieres compartir tus experiencias, ¡deja un comentario abajo!

Próxima vez en Rustaceo: Exploraremos “Manejo de Errores en Rust: Resultados y Opciones”, continuando nuestra emocionante aventura en el mundo de Rust.

¡Hasta la próxima, entrenadores y entusiastas de Rust!


Recursos adicionales #


¡Atrapa todos los conocimientos y feliz programación!