¡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
, declaramositem1
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:
- Retornar el valor en lugar de una referencia.
- 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!