6 - Préstamos y referencias en Rust - gestión segura de memoria

6 - Préstamos y referencias en Rust - gestión segura de memoria

¡Bienvenidos de nuevo a Rustaceo.es! En nuestra última entrega, desmitificamos el concepto de propiedad en Rust. Hoy, profundizaremos en préstamos y referencias, dos pilares fundamentales que permiten gestionar la memoria de forma segura y eficiente en Rust. Acompáñame mientras exploramos cómo estas características nos ayudan a escribir código robusto y libre de errores, todo mientras entrenamos a nuestros Pokémon favoritos.

Repaso: la propiedad en Rust #

Antes de sumergirnos en préstamos y referencias, recordemos que en Rust:

  • Cada valor tiene un único propietario.
  • Cuando el propietario sale del alcance, el valor se elimina.

Esto garantiza que no haya fugas de memoria ni accesos a memoria inválida. Sin embargo, a veces necesitamos que múltiples partes de nuestro programa accedan a un valor sin transferir su propiedad. Aquí es donde entran en juego los préstamos y las referencias.

¿Qué son las referencias y los préstamos? #

Una referencia es una forma de prestar acceso a un valor sin tomar su propiedad. Las referencias nos permiten leer o modificar un valor mientras mantenemos las reglas de seguridad de Rust.

Hay dos tipos principales de referencias:

  • Referencias Inmutables (&T): Permiten leer un valor sin modificarlo.
  • Referencias Mutables (&mut T): Permiten modificar un valor.

Reglas de los préstamos #

  1. Puedes tener cualquier número de referencias inmutables a un dato.
  2. Solo puedes tener una referencia mutable a un dato en un momento dado.
  3. No puedes tener referencias mutables y referencias inmutables al mismo tiempo.

Estas reglas evitan condiciones de carrera y aseguran la seguridad en tiempo de compilación.

Referencias inmutables con pokémon #

Imaginemos que queremos consultar información sobre un Pokémon sin modificarlo.

Ejemplo: Consultar Datos de un Pokémon

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

fn mostrar_elemento(pokemon: &Pokémon) {
    println!("Nombre: {}", pokemon.nombre);
    println!("Nivel: {}", pokemon.nivel);
}

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

    mostrar_elemento(&item1); // Pasamos una referencia inmutable
    // Podemos seguir usando item1 aquí
    println!("Seguimos entrenando a {}.", item1.nombre);
}

En este ejemplo:

  • &Pokémon es una referencia inmutable a Pokémon.
  • Podemos llamar a mostrar_elemento y luego seguir usando item1 porque no hemos transferido su propiedad.

Referencias mutables con pokémon #

Si necesitamos modificar los datos de un Pokémon, usamos una referencia mutable.

Ejemplo: Entrenar a un Pokémon (Subir de Nivel)

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

fn entrenar_elemento(pokemon: &mut Pokémon) {
    pokemon.nivel += 1;
    println!({} ha subido al nivel {}!", pokemon.nombre, pokemon.nivel);
}

fn main() {
    let mut item2 = Pokémon {
        nombre: String::from("Item2"),
        nivel: 12,
    };

    entrenar_elemento(&mut item2); // Pasamos una referencia mutable
    // Podemos seguir usando item2 aquí
    println!("El nivel actual de {} es {}.", item2.nombre, item2.nivel);
}

En este ejemplo:

  • &mut Pokémon es una referencia mutable a Pokémon.
  • Podemos modificar item2 dentro de entrenar_elemento.
  • Debemos declarar item2 como mut en main para poder prestarlo mutablemente.

Importante: solo una referencia mutable a la vez #

Intentar crear múltiples referencias mutables al mismo tiempo resultará en un error de compilación.

Ejemplo de Error:

let mut item1 = Pokémon {
    nombre: String::from("Item1"),
    nivel: 25,
};

let ref1 = &mut item1;
let ref2 = &mut item1; // Error: segunda referencia mutable

// Uso de ref1 y ref2

Mensaje de error:

error[E0499]: cannot borrow `item1` as mutable more than once at a time

Combinando referencias inmutables y mutables #

No podemos tener referencias mutables y referencias inmutables al mismo tiempo para evitar condiciones de carrera.

Ejemplo Incorrecto:

let mut item4 = Pokémon {
    nombre: String::from("Item4"),
    nivel: 15,
};

let ref_inmutable1 = &item4;
let ref_inmutable2 = &item4;
let ref_mutable = &mut item4; // Error: referencias inmutables aún están en uso

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

Mensaje de error:

error[E0502]: cannot borrow `item4` as mutable because it is also borrowed as immutable

Solución:

Asegurarnos de que no haya referencias inmutables activas cuando creamos una referencia mutable.

Tiempo de vida (lifetimes) de las referencias #

Las referencias en Rust tienen un tiempo de vida que define cuánto tiempo viven en el programa. El compilador de Rust utiliza el análisis de tiempo de vida para garantizar que las referencias sean siempre válidas.

Ejemplo: tiempo de vida válido #

fn main() {
    let item1 = String::from("Item1");

    {
        let entrenador = &item1; // Referencia inmutable
        println!("El entrenador tiene a {}.", entrenador);
    } // `entrenador` sale del alcance aquí

    println!("Seguimos con {}.", item1);
}

Aquí, entrenador es una referencia válida porque no vive más allá de item1.

Ejemplo: tiempo de vida inválido #

fn main() {
    let entrenador;

    {
        let item1 = String::from("Item1");
        entrenador = &item1; // Referencia a `item1`
    } // `item1` sale del alcance aquí

    // println!("El entrenador tiene a {}.", entrenador); // Error: `entrenador` referencia a un valor que ya no existe
}

Mensaje de error:

error[E0597]: `item1` does not live long enough

Práctica: centro pokémon mejorado #

Supongamos que queremos crear una función que cure a varios Pokémon.

Ejemplo: Curar Múltiples Pokémon

struct Pokémon {
    nombre: String,
    salud: u8,
}

fn curar_elemento(pokemon: &mut Pokémon) {
    pokemon.salud = 100;
    println!("{} ha sido curado.", pokemon.nombre);
}

fn curar_equipo(equipo: &mut Vec<Pokémon>) {
    for pokemon in equipo {
        curar_elemento(pokemon);
    }
}

fn main() {
    let mut equipo = vec![
        Pokémon {
            nombre: String::from("Item1"),
            salud: 50,
        },
        Pokémon {
            nombre: String::from("Item3"),
            salud: 30,
        },
        Pokémon {
            nombre: String::from("Item2"),
            salud: 20,
        },
    ];

    curar_equipo(&mut equipo);

    for pokemon in &equipo {
        println!("{} tiene {} puntos de salud.", pokemon.nombre, pokemon.salud);
    }
}

En este código:

  • Usamos una referencia mutable al vector equipo para modificar los Pokémon dentro de él.
  • La función curar_elemento recibe una referencia mutable a cada Pokémon.

Slices (cortes o rebanadas) #

Las slices nos permiten trabajar con partes de colecciones sin tomar posesión.

Ejemplo: Obteniendo una Sublista de Pokémon

let equipo = vec!["Item1", "Item4", "Item2", "Item3"];

let iniciales = &equipo[0..3]; // Slice de los primeros tres Pokémon

for pokemon in iniciales {
    println!("Pokémon inicial: {}", pokemon);
}

Las slices son referencias, por lo que siguen las reglas de préstamos.

Referencias y funciones de retorno #

Al retornar referencias desde funciones, debemos asegurarnos de que las referencias sean válidas.

Ejemplo Correcto:

fn obtener_nombre_mayor(elemento1: &Pokémon, elemento2: &Pokémon) -> &String {
    if elemento1.nombre.len() > elemento2.nombre.len() {
        &elemento1.nombre
    } else {
        &elemento2.nombre
    }
}

Aquí, ambas referencias pasadas a la función tienen el mismo tiempo de vida, por lo que la referencia retornada es válida.

Parámetros de tiempo de vida explícitos #

A veces, necesitamos especificar los tiempos de vida de las referencias.

Ejemplo:

fn obtener_nombre<'a>(pokemon: &'a Pokémon) -> &'a String {
    &pokemon.nombre
}

Aquí, 'a es un parámetro de tiempo de vida que asegura que la referencia retornada no viva más que pokemon. Veremos los tiempos de vida más en profundidad en un artículo futuro, de momento no te agobies por comprender esta sintaxis al 100%, lo importante es comprender que no pueden quedar referencias a valores que ya no son válidos.

Conclusión #

Los préstamos y referencias en Rust son herramientas poderosas que nos permiten manejar la memoria de forma segura y eficiente. Al entender y aplicar las reglas de préstamos, podemos evitar errores comunes como referencias nulas o condiciones de carrera.

Usando ejemplos de Pokémon, hemos explorado cómo funcionan las referencias inmutables y mutables, el concepto de tiempo de vida, y cómo aplicarlos en programas reales. Te animo a practicar estos conceptos en tus propios proyectos y a experimentar con diferentes escenarios.


¿Te ha sido útil este artículo? ¡Comparte tus preguntas y experiencias en los comentarios! En nuestra próxima entrega de Rustaceo, profundizaremos en “Entendiendo Lifetimes en Rust: Garantizando Referencias Válidas”.

¡Sigue explorando y codificando en el mundo de Rust y Pokémon!