7 - Ejercicios de propiedad - superando desafíos en Rust con pokémon

7 - Ejercicios de propiedad - superando desafíos en Rust con pokémon

¡Bienvenidos de nuevo a Rustaceo.es! En las entregas anteriores, exploramos los conceptos fundamentales de propiedad, préstamos y referencias en Rust. Ahora es el momento de poner a prueba nuestros conocimientos con una serie de ejercicios prácticos que nos ayudarán a consolidar lo aprendido y a superar los desafíos comunes relacionados con la propiedad en Rust. ¡Prepárate para entrenar tus habilidades de programación al máximo nivel!

Objetivo de los ejercicios #

  • Aplicar los conceptos de propiedad y préstamos en situaciones prácticas.
  • Identificar y resolver errores comunes relacionados con la propiedad.
  • Mejorar la comprensión de las reglas de Rust para escribir código más seguro y eficiente.

Ejercicio 1: transferencia de propiedad entre entrenadores #

Descripción #

Ash y Misty quieren intercambiar Pokémon. Ash tiene un Item1, y Misty tiene un Psyduck. Implementa el intercambio asegurándote de que no haya problemas de propiedad.

Código inicial #

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

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

    let psyduck = Pokémon {
        nombre: String::from("Psyduck"),
        nivel: 20,
    };

    // Intercambio de Pokémon
    let ash_elemento = item1;
    let misty_elemento = psyduck;

    // Aquí intentamos usar item1 y psyduck nuevamente
    println!("Ash tiene a {}.", item1.nombre);
    println!("Misty tiene a {}.", psyduck.nombre);
}

Problema #

El código anterior generará errores de compilación relacionados con la propiedad. Identifica y corrige los errores para que ambos entrenadores puedan intercambiar sus Pokémon y el programa pueda imprimir correctamente los resultados.

Solución #

El problema es que al asignar item1 a ash_elemento, estamos transfiriendo la propiedad de item1 a ash_elemento. Por lo tanto, ya no podemos usar item1 después de esa línea. Lo mismo ocurre con psyduck y misty_elemento.

Corrección:

Podemos utilizar el método .clone() para crear copias de los Pokémon si queremos seguir usando las variables originales.

#[Derive(clone)]
struct Pokémon {
    nombre: String,
    nivel: u8,
}

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

    let psyduck = Pokémon {
        nombre: String::from("Psyduck"),
        nivel: 20,
    };

    // Intercambio de Pokémon utilizando clone()
    let ash_elemento = item1.clone();
    let misty_elemento = psyduck.clone();

    println!("Ash tiene a {}.", ash_elemento.nombre);
    println!("Misty tiene a {}.", misty_elemento.nombre);

    // Podemos seguir usando item1 y psyduck
    println!("Originalmente, Ash tenía a {}.", item1.nombre);
    println!("Originalmente, Misty tenía a {}.", psyduck.nombre);
}

Nota: Para usar .clone(), debemos asegurarnos de que la estructura Pokémon implemente el trait Clone. Agregamos #[derive(Clone)] a la estructura.

Resultado #

El programa ahora compila y se ejecuta correctamente, mostrando:

Ash tiene a Item1.
Misty tiene a Psyduck.
Originalmente, Ash tenía a Item1.
Originalmente, Misty tenía a Psyduck.

Ejercicio 2: gestión de referencias mutables #

Descripción #

Queremos crear una función que aumente el nivel de un Pokémon en 1. Sin embargo, al intentar modificar el Pokémon, encontramos problemas relacionados con referencias mutables.

Código inicial #

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

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

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

    subir_nivel(&item1);
}

Problema #

El código anterior genera un error porque estamos intentando modificar un valor a través de una referencia inmutable.

Mensaje de error:

error[E0594]: cannot assign to `pokemon.nivel`, as `pokemon` is a `&` reference

Solución #

Necesitamos cambiar la referencia a mutable tanto en la función como en la llamada.

Corrección:

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);
}
  • En la función subir_nivel, cambiamos &Pokémon a &mut Pokémon.
  • En main, declaramos item1 como mutable (let mut item1) y pasamos una referencia mutable (&mut item1).

Resultado #

El programa ahora compila y muestra:

¡Item1 ha subido al nivel 26!

Ejercicio 3: evitando referencias dangling (colgantes o caducas) #

Descripción #

Intentamos retornar una referencia a un valor creado dentro de una función, pero Rust no lo permite debido a que la referencia sería inválida fuera del alcance de la función.

Código inicial #

fn obtener_elemento() -> &String {
    let nombre = String::from("Eevee");
    &nombre
}

fn main() {
    let pokemon = obtener_elemento();
    println!("Has obtenido a {}.", pokemon);
}

Problema #

El código no compila porque estamos retornando una referencia a una variable (nombre) que ya no existe después de que la función obtener_elemento termina.

Mensaje de error:

error[E0515]: cannot return reference to local variable `nombre`

Solución #

Existen dos soluciones:

  1. Retornar el valor en lugar de una referencia.
  2. Usar el tipo de dato String directamente.

Corrección utilizando la primera opción:

fn obtener_elemento() -> String {
    let nombre = String::from("Eevee");
    nombre // Retornamos el valor, moviendo la propiedad
}

fn main() {
    let pokemon = obtener_elemento();
    println!("Has obtenido a {}.", pokemon);
}

Ahora, estamos moviendo la propiedad de nombre al llamador, lo cual es seguro.

Resultado #

El programa compila y muestra:

Has obtenido a Eevee.

Ejercicio 4: múltiples referencias inmutables y mutables #

Descripción #

Estamos intentando leer y escribir en un Pokémon al mismo tiempo, pero Rust nos advierte sobre violaciones de las reglas de préstamos.

Código inicial #

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

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

    let ref1 = &item4;
    let ref2 = &mut item4;

    println!("{} está en el nivel {}.", ref1.nombre, ref1.nivel);
    ref2.nivel += 1;
    println!("{} ha subido al nivel {}.", ref2.nombre, ref2.nivel);
}

Problema #

No podemos tener una referencia mutable (ref2) mientras existen referencias inmutables activas (ref1).

Mensaje de error:

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

Solución #

Necesitamos asegurarnos de que no hay referencias inmutables en uso cuando creamos una referencia mutable.

Corrección:

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

    {
        let ref1 = &item4;
        println!("{} está en el nivel {}.", ref1.nombre, ref1.nivel);
        // ref1 sale de alcance aquí
    }

    {
        let ref2 = &mut item4;
        ref2.nivel += 1;
        println!("{} ha subido al nivel {}.", ref2.nombre, ref2.nivel);
        // ref2 sale de alcance aquí
    }
}

Al limitar el alcance de las referencias, nos aseguramos de que no haya conflictos.

Resultado #

El programa compila y muestra:

Item4 está en el nivel 15.
Item4 ha subido al nivel 16.

Ejercicio 5: uso correcto de slices (rebanadas) #

Descripción #

Queremos obtener una subcadena del nombre de un Pokémon, pero encontramos problemas relacionados con los tiempos de vida.

Código inicial #

fn obtener_inicial(nombre: &String) -> &str {
    &nombre[0..1]
}

fn main() {
    let nombre_elemento = String::from("Item2");
    let inicial = obtener_inicial(&nombre_elemento);
    println!("La inicial de {} es {}.", nombre_elemento, inicial);
}

Problema #

El código compila y funciona, pero es importante entender cómo funcionan los tiempos de vida para evitar futuros errores.

Solución #

PENDIENTE

Resultado #

El programa sigue funcionando y muestra:

La inicial de Item2 es C.

Reflexiones finales #

Estos ejercicios nos han permitido enfrentar y superar desafíos comunes relacionados con la propiedad y los préstamos en Rust. Al practicar y resolver estos problemas, hemos consolidado nuestra comprensión de cómo Rust maneja la memoria de forma segura.

Lecciones Aprendidas:

  • Clonar Valores Cuando Sea Necesario: Usar .clone() para crear copias y evitar problemas de propiedad.
  • Usar Referencias Mutables con Precaución: Respetar las reglas de préstamos para evitar conflictos.
  • Retornar Valores en Lugar de Referencias a Variables Locales: Evitar referencias colgantes retornando valores o utilizando estructuras que vivan lo suficiente.
  • Gestionar Alcances para Referencias: Limitar el alcance de las referencias para cumplir con las reglas de préstamos.
  • Especificar Tiempos de Vida Cuando Sea Necesario: Ayudar al compilador y al lector del código a entender las relaciones entre referencias.

¿Te resultaron útiles estos ejercicios? Te animo a crear tus propios desafíos y a seguir practicando. La mejor manera de dominar Rust es enfrentando y resolviendo problemas del mundo real.

En la próxima entrega de Rustaceo, exploraremos “Implementando Estructuras de Datos en Rust: Listas Enlazadas y Árboles Binarios” utilizando nuestros fieles Pokémon.

¡Sigue entrenando y codificando!