5 - Desmitificando la propiedad en Rust - gestionando la memoria con pokémon

5 - Desmitificando la propiedad en Rust - gestionando la memoria con pokémon

¡Bienvenidos de nuevo a Rustaceo.es esta entrega, abordaremos uno de los conceptos más fundamentales y, a veces, desafiantes de Rust: la propiedad. Utilizando ejemplos del mundo Pokémon, trataré de explicar cómo Rust maneja la memoria de forma segura y eficiente. Si alguna vez te has preguntado cómo evitar errores comunes relacionados con la gestión de memoria, seguro que este artículo es para ti.

¿Qué es la propiedad en Rust? #

La propiedad es un conjunto de reglas que gobiernan cómo Rust gestiona la memoria. A diferencia de otros lenguajes que utilizan recolección de basura o recuento de referencias, Rust garantiza seguridad en tiempo de compilación mediante un sistema de propiedad único.

Las tres reglas de la propiedad #

  1. Cada valor en Rust tiene un propietario.
  2. Solo puede haber un propietario a la vez.
  3. Cuando el propietario sale del alcance o scope, el valor se elimina.

Explorando la propiedad con pokémon #

Imaginemos que estamos manejando nuestros Pokémon como valores en Rust. Cada Pokémon tiene un entrenador (propietario). Veamos cómo funciona esto en código.

Ejemplo: transferencia de propiedad #

struct Pokémon {  
    nombre: String,  
    nivel: u8,  
}  
  
fn main() {  
    let item1 = Pokémon {  
        nombre: String::from("Item1"),  
        nivel: 25,  
    };  
  
    let nuevo_item1 = item1; // La propiedad de item1 se transfiere a nuevo_item1  
  
    println!("Item1 está en el nivel {}.", item1.nivel);  
    // Error: valor movido, no se puede usar item1 aquí  
}

En este ejemplo:

  • Paso 1: Creamos un Pokémon llamado item1.
  • Paso 2: Asignamos item1 a nuevo_item1, transfiriendo la propiedad.
  • Paso 3: Si intentamos usar item1 después de la transferencia, Rust generará un error.

Mensaje de error:

error[E0382]: borrow of moved value: `item1`

Esto ocurre porque la propiedad de item1 ha sido movida a nuevo_item1, y no podemos usar item1 después de eso.

Clonado de valores #

Si queremos que ambos, item1 y nuevo_item1, tengan sus propias copias, necesitamos clonar el valor.

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

let nuevo_item1 = item1.clone(); // Clonamos item1

println!("Item1 está en el nivel {}.", item1.nivel);
println!("El nuevo Item1 es {} de nivel {}.", nuevo_item1.nombre, nuevo_item1.nivel);

Ahora, tanto item1 como nuevo_item1 tienen sus propias instancias independientes.

Referencias y préstamos #

Para permitir que múltiples partes del código accedan a un valor sin transferir la propiedad, usamos referencias.

Referencias inmutables #

Podemos prestar una referencia inmutable a un valor, permitiendo que otros lo lean pero no lo modifiquen.

Ejemplo: Consultar Información de un Pokémon

fn mostrar_nivel(pokemon: &Pokémon) {
    println!("{} está en el nivel {}.", pokemon.nombre, pokemon.nivel);
}

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

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

Referencias mutables #

Si necesitamos modificar un valor, podemos prestar una referencia mutable.

Reglas para Referencias Mutables:

  • Solo puede haber una referencia mutable a un valor en un momento dado.
  • No puede coexistir una referencia mutable con referencias inmutables al mismo tiempo.

Ejemplo: Subir de Nivel a un Pokémon

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

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

    subir_nivel(&mut item1); // Pasamos una referencia mutable

    // No podemos tener otra referencia mutable aquí hasta que la anterior termine
    println!("Seguimos entrenando a {}.", item1.nombre);
}

Tiempo de vida (lifetimes) #

Las referencias en Rust también tienen tiempos de vida que aseguran que no accedamos a datos que ya no existen.

Ejemplo: Referencias y Alcance

fn main() {
    let entrenador;

    {
        let item1 = Pokémon {
            nombre: String::from("Item1"),
            nivel: 25,
        };
        entrenador = &item1.nombre;
        // item1 sale del alcance aquí
    }

    // Intentamos usar entrenador
    // println!("El entrenador tiene a {}.", entrenador);
    // Error: referencia a un valor fuera de alcance
}

Mensaje de error:

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

Esto sucede porque item1 sale del alcance antes que entrenador, dejando a entrenador con una referencia inválida.

Propiedad en colecciones #

Las colecciones como Vec<T> también siguen las reglas de propiedad.

Ejemplo: Equipo Pokémon

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

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

    let charizard = Pokémon {
        nombre: String::from("Charizard"),
        nivel: 36,
    };

    let mut equipo = Vec::new();
    equipo.push(item1);
    equipo.push(charizard);

    // item1 y charizard han sido movidos al vector equipo
    // No podemos usarlos directamente aquí
    // println!("Item1 está en el nivel {}.", item1.nivel);
}

Para acceder a los Pokémon en el equipo, iteramos sobre el vector:

for pokemon in &equipo {
    println!("{} está en el nivel {}.", pokemon.nombre, pokemon.nivel);
}

Evitando errores comunes #

Error: uso después de mover #

let item1 = String::from("Item1");
let entrenador_ash = item1;

// println!("Nuestro Pokémon es {}.", item1); // Error

Solución: Usar una referencia o clonar el valor.

Error: múltiples referencias mutables #

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

let ref1 = &mut item1;
let ref2 = &mut item1; // Error

// ref1 y ref2 no pueden coexistir

Solución: Asegurarse de que solo haya una referencia mutable en uso.

Práctica: centro pokémon #

Imaginemos un escenario donde los entrenadores dejan sus Pokémon en un Centro Pokémon para curarlos.

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

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

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

    curar(&mut item1);

    println!("Estado actual de {}: {} puntos de salud.", item1.nombre, item1.salud);
}

Aquí, pasamos una referencia mutable de item1 a la función curar, permitiendo modificar su salud.

Conclusión #

La propiedad y el préstamo en Rust pueden parecer complicados al principio, pero son fundamentales para garantizar la seguridad y eficiencia del manejo de memoria. Al entender estos conceptos, puedes escribir código más robusto y libre de errores comunes como referencias nulas o fugas de memoria.

Al utilizar ejemplos de Pokémon, hemos desmitificado cómo funcionan la propiedad y las referencias en Rust. Te animo a practicar estos conceptos en tus propios programas y a experimentar con diferentes escenarios.


¿Te ha resultado útil este artículo? ¡Comparte tus experiencias y preguntas en los comentarios! En la próxima entrega de Rustaceo, exploraremos “Referencias y Tiempo de Vida: Profundizando en la Seguridad de Rust”.

¡Sigue entrenando y codificando, entrenadores y desarrolladores!