¡Bienvenidos de nuevo a Rustaceo.es! En la Parte 1, comenzamos a implementar una lista enlazada simple en Rust. Como recordarás, construimos una estructura básica que nos permitía insertar Pokémon al frente de la lista y eliminar el primer pokémon. En esta segunda parte, profundizaremos en conceptos más avanzados y mejoraremos nuestra lista enlazada. Abordaremos desafíos relacionados con los tiempos de vida, implementaremos iteradores y agregaremos nuevas funcionalidades.
Objetivos de esta parte #
- Implementar iteradores para nuestra lista enlazada.
- Manejar correctamente los tiempos de vida y las referencias.
- Agregar métodos adicionales como inserción al final y búsqueda.
- Mejorar la seguridad y eficiencia de nuestra estructura.
Repaso de la estructura actual #
Recordemos la estructura de nuestra lista enlazada:
#[Derive(debug)]
struct Pokémon {
nombre: String,
tipo: String,
nivel: u8,
}
type Link = Option<Box<Nodo>>;
struct Nodo {
pokemon: Pokémon,
siguiente: Link,
}
pub struct ListaEnlazada {
cabeza: Link,
}
impl ListaEnlazada {
pub fn nueva() -> Self {
ListaEnlazada { cabeza: None }
}
pub fn insertar_al_frente(&mut self, pokemon: Pokémon) {
let nuevo_nodo = Nodo {
pokemon,
siguiente: self.cabeza.take(),
};
self.cabeza = Some(Box::new(nuevo_nodo));
}
pub fn pop(&mut self) -> Option<Pokémon> {
self.cabeza.take().map(|nodo| {
self.cabeza = nodo.siguiente;
nodo.pokemon
})
}
pub fn imprimir_lista(&self) {
let mut actual = &self.cabeza;
while let Some(nodo) = actual {
println!(
"{} - Tipo: {}, Nivel: {}",
nodo.pokemon.nombre, nodo.pokemon.tipo, nodo.pokemon.nivel
);
actual = &nodo.siguiente;
}
}
}
Implementando un iterador para la lista enlazada #
Para poder recorrer nuestra lista de forma más idiomática en Rust, implementaremos el trait Iterator
. Esto nos permitirá usar for
loops y otras funcionalidades que esperan iteradores.
Paso 1: definir la estructura del iterador #
Crearemos una estructura Iter
que contendrá una referencia a la próxima posición en la lista.
pub struct Iter<'a> {
actual: Option<&'a Nodo>,
}
Aquí, 'a
es un tiempo de vida que asegura que las referencias dentro del iterador no vivan más que la lista original.
Paso 2: implementar un método para obtener el iterador #
En ListaEnlazada
, agregamos un método iter
que retorna una instancia de Iter
.
impl ListaEnlazada {
// ... código anterior ...
pub fn iter(&self) -> Iter {
Iter { actual: self.cabeza.as_deref() }
}
}
as_deref()
convierteOption<&Box<Nodo>>
enOption<&Nodo>
.
Paso 3: implementar el trait iterator
para iter
#
Implementamos el trait Iterator
para nuestra estructura Iter
.
impl<'a> Iterator for Iter<'a> {
type Item = &'a Pokémon;
fn next(&mut self) -> Option<Self::Item> {
self.actual.map(|nodo| {
self.actual = nodo.siguiente.as_deref();
&nodo.pokemon
})
}
}
- En el método
next
, utilizamosmap
para acceder al nodo actual. - Actualizamos
self.actual
para que apunte al siguiente nodo. - Retornamos una referencia al
pokemon
actual.
Paso 4: usar el iterador en main
#
Probemos el iterador en nuestro main
.
fn main() {
// ... código para crear la lista ...
println!("Recorriendo la lista con un iterador:");
for pokemon in lista.iter() {
println!(
"{} - Tipo: {}, Nivel: {}",
pokemon.nombre, pokemon.tipo, pokemon.nivel
);
}
}
Resultado Esperado:
Recorriendo la lista con un iterador:
Item2 - Tipo: Fuego, Nivel: 12
Item4 - Tipo: Planta/Veneno, Nivel: 15
Item1 - Tipo: Eléctrico, Nivel: 25
Implementando un iterador mutable #
También podemos querer iterar sobre nuestra lista y modificar los elementos. Para ello, necesitamos un iterador mutable.
Paso 1: definir la estructura del iterador mutable #
pub struct IterMut<'a> {
actual: Option<&'a mut Nodo>,
}
Paso 2: implementar el método iter_mut
#
En ListaEnlazada
, agregamos:
impl ListaEnlazada {
// ... código anterior ...
pub fn iter_mut(&mut self) -> IterMut {
IterMut { actual: self.cabeza.as_deref_mut() }
}
}
as_deref_mut()
convierteOption<&mut Box<Nodo>>
enOption<&mut Nodo>
.
Paso 3: implementar el trait iterator
para itermut
#
impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut Pokémon;
fn next(&mut self) -> Option<Self::Item> {
self.actual.take().map(|nodo| {
self.actual = nodo.siguiente.as_deref_mut();
&mut nodo.pokemon
})
}
}
Paso 4: usar el iterador mutable #
En main
, podemos usar el iterador mutable para subir de nivel a todos los Pokémon.
for pokemon in lista.iter_mut() {
pokemon.nivel += 1;
}
Y luego imprimir la lista actualizada:
println!("Lista después de subir de nivel a los Pokémon:");
lista.imprimir_lista();
Resultado Esperado:
Lista después de subir de nivel a los Pokémon:
Item2 - Tipo: Fuego, Nivel: 13
Item4 - Tipo: Planta/Veneno, Nivel: 16
Item1 - Tipo: Eléctrico, Nivel: 26
Agregando un método para insertar al final #
Hasta ahora, solo podemos insertar al frente de la lista. Implementemos un método insertar_al_final
.
Paso 1: implementar insertar_al_final
#
impl ListaEnlazada {
// ... código anterior ...
pub fn insertar_al_final(&mut self, pokemon: Pokémon) {
let mut enlace = &mut self.cabeza;
while enlace.is_some() {
enlace = &mut enlace.as_mut().unwrap().siguiente;
}
*enlace = Some(Box::new(Nodo {
pokemon,
siguiente: None,
}));
}
}
Explicación:
- Utilizamos un puntero mutable
enlace
que empieza enself.cabeza
. - Iteramos hasta encontrar el
None
al final de la lista. - Asignamos un nuevo
Nodo
en ese lugar.
Paso 2: probar insertar_al_final
#
En main
, agregamos:
let item3 = Pokémon {
nombre: String::from("Item3"),
tipo: String::from("Agua"),
nivel: 10,
};
lista.insertar_al_final(item3);
println!("Lista después de insertar a Item3 al final:");
lista.imprimir_lista();
Resultado Esperado:
Lista después de insertar a Item3 al final:
Item2 - Tipo: Fuego, Nivel: 13
Item4 - Tipo: Planta/Veneno, Nivel: 16
Item1 - Tipo: Eléctrico, Nivel: 26
Item3 - Tipo: Agua, Nivel: 10
Agregando un método para buscar un pokemon #
Implementemos un método que nos permita buscar un Pokémon por nombre.
Paso 1: implementar buscar_elemento
#
impl ListaEnlazada {
// ... código anterior ...
pub fn buscar_elemento(&self, nombre: &str) -> Option<&Pokémon> {
let mut actual = &self.cabeza;
while let Some(nodo) = actual {
if nodo.pokemon.nombre == nombre {
return Some(&nodo.pokemon);
}
actual = &nodo.siguiente;
}
None
}
}
Paso 2: probar buscar_elemento
#
En main
, agregamos:
if let Some(pokemon) = lista.buscar_elemento("Item1") {
println!(
"Encontrado: {} - Tipo: {}, Nivel: {}",
pokemon.nombre, pokemon.tipo, pokemon.nivel
);
} else {
println!("Pokémon no encontrado.");
}
Resultado Esperado:
Encontrado: Item1 - Tipo: Eléctrico, Nivel: 26
Manejando tiempos de vida y seguridad #
Al implementar iteradores y métodos adicionales, es importante asegurarse de que nuestras referencias y tiempos de vida sean correctos para evitar errores en tiempo de compilación.
Importancia de los tiempos de vida #
- Los tiempos de vida (
lifetimes
) aseguran que las referencias no vivan más allá de los datos a los que apuntan. - Al implementar iteradores, especificamos los tiempos de vida para vincular las referencias al tiempo de vida de la lista.
Ejemplo de tiempos de vida en el iterador #
pub struct Iter<'a> {
actual: Option<&'a Nodo>,
}
- El tiempo de vida
'a
indica queIter
no puede vivir más que la referencia aListaEnlazada
.
Mejorando la eficiencia con rc
y refcell
#
Hasta ahora, nuestra lista es unidireccional y no permite múltiples propietarios ni mutabilidad interior. Si queremos una lista más flexible, podemos usar Rc<T>
y RefCell<T>
.
Advertencia #
- Usar
Rc
yRefCell
agrega complejidad y puede introducir riesgos si no se maneja correctamente. - Para este ejemplo, mantendremos nuestra implementación simple y segura.
Resumen de la segunda parte #
En esta segunda parte, hemos:
- Implementado iteradores para recorrer nuestra lista de forma idiomática.
- Agregado métodos adicionales como
insertar_al_final
ybuscar_elemento
. - Manejado tiempos de vida para asegurar la seguridad de nuestras referencias.
- Mejorado nuestra comprensión de las estructuras de datos y cómo implementarlas en Rust.
Próximos pasos #
Para seguir mejorando nuestra lista enlazada:
- Implementar métodos de eliminación más específicos, como eliminar un Pokémon por nombre.
- Convertir la lista en doblemente enlazada, permitiendo iteración en ambos sentidos.
- Utilizar patrones de diseño para mejorar la modularidad y reutilización del código.
¿Te ha resultado útil este artículo? Implementar estructuras de datos en Rust es una excelente manera de profundizar en el lenguaje y comprender cómo maneja la memoria y las referencias. Te animo a seguir experimentando y expandiendo la funcionalidad de tu lista enlazada.
Si tienes preguntas o deseas compartir tus avances, ¡deja un comentario abajo!
Hasta la próxima en Rustaceo, donde continuaremos explorando el fascinante mundo de Rust y la programación de sistemas.