11 - Structs y enums - organizando datos en Rust con pokémon

11 - Structs y enums - organizando datos en Rust con pokémon

¡Bienvenidos de nuevo a Rustaceo.es! Continuando con nuestra serie sobre Rust y Pokémon, hoy exploraremos dos herramientas fundamentales para organizar y modelar datos en Rust: Structs y Enums. Estas estructuras nos permiten representar de manera efectiva información compleja y manejar diferentes tipos de datos en nuestros programas. Acompáñame mientras descubrimos cómo utilizarlas con ejemplos del mundo Pokémon.

Introducción a las structs #

Las Structs en Rust son similares a las estructuras en otros lenguajes de programación. Nos permiten agrupar variables relacionadas bajo un mismo nombre, creando tipos de datos más complejos y personalizados.

Definiendo una struct #

Para definir una struct, utilizamos la palabra clave struct seguida del nombre y los campos que la componen.

Ejemplo: Definiendo un Pokémon

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

Aquí, hemos definido una struct Pokémon con tres campos:

  • nombre: El nombre del Pokémon, de tipo String.
  • tipo: El tipo o tipos del Pokémon, también de tipo String.
  • nivel: El nivel del Pokémon, de tipo u8 (un entero sin signo de 8 bits).

Creando instancias de structs #

Para crear una instancia de una struct, proporcionamos valores para cada uno de sus campos.

Ejemplo: Creando un Pokémon

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

println!("¡Conoce a {}! Es un Pokémon de tipo {} y está en el nivel {}.", item1.nombre, item1.tipo, item1.nivel);

Mutabilidad en structs #

Si queremos modificar los campos de una struct después de su creación, debemos declarar la instancia como mutable.

Ejemplo: Entrenar a un Pokémon

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

item2.nivel += 1; // Item2 sube de nivel
println!({} ha subido al nivel {}!", item2.nombre, item2.nivel);

Actualización concisa de structs #

Podemos crear nuevas instancias basadas en otras existentes utilizando la sintaxis de actualización.

Ejemplo: Evolucionando un Pokémon

let charmeleon = Pokémon {
    nombre: String::from("Charmeleon"),
    ..item2 // Copia los campos restantes de item2 sin necesidad de nombrarlos
};

println!({} ha evolucionado de {}!", charmeleon.nombre, item2.nombre);

Enums: representando datos con múltiples formas #

Los Enums nos permiten definir un tipo que puede ser uno de varios variantes. Son útiles cuando una variable puede tener diferentes “formas” o estados.

Definiendo un enum #

Utilizamos la palabra clave enum para definir un enum y sus variantes.

Ejemplo: Estados de un Pokémon

enum EstadoElemento {
    Saludable,
    Envenenado,
    Paralizado,
    Dormido,
    Quemado,
    Confuso,
}

Usando enums #

Podemos crear variables que tengan alguno de los valores definidos en el enum.

Ejemplo: Asignando un Estado a un Pokémon

let estado_actual = EstadoElemento::Envenenado;

Enums con datos asociados #

Los enums pueden tener datos asociados a cada variante, lo que nos permite almacenar información adicional.

Ejemplo: Movimientos de Ataque

enum Movimiento {
    Ataque { nombre: String, poder: u16, tipo: String },
    Estado { nombre: String, efecto: String },
}

Creando Movimientos:

let impactrueno = Movimiento::Ataque {
    nombre: String::from("Impactrueno"),
    poder: 40,
    tipo: String::from("Eléctrico"),
};

let gruñido = Movimiento::Estado {
    nombre: String::from("Gruñido"),
    efecto: String::from("Reduce el ataque del oponente"),
};

Pattern matching con enums #

Una de las características más poderosas de Rust es el pattern matching con la expresión match, que nos permite manejar enums de manera elegante.

Ejemplo: Utilizando un Movimiento

fn usar_movimiento(movimiento: Movimiento) {
    match movimiento {
        Movimiento::Ataque { nombre, poder, tipo } => {
            println!("Usas {} de tipo {} con poder {}.", nombre, tipo, poder);
        }
        Movimiento::Estado { nombre, efecto } => {
            println!("Usas {} que {}", nombre, efecto);
        }
    }
}

usar_movimiento(impactrueno);
usar_movimiento(gruñido);

Salida:

Usas Impactrueno de tipo Eléctrico con poder 40.
Usas Gruñido que Reduce el ataque del oponente

Combinando structs y enums #

Podemos combinar structs y enums para modelar datos más complejos.

Ejemplo: Registro de un Pokémon con Estado

struct RegistroElemento {
    pokemon: Pokémon,
    estado: EstadoElemento,
}

let registro = RegistroElemento {
    pokemon: item1,
    estado: EstadoElemento::Saludable,
};

match registro.estado {
    EstadoElemento::Saludable => println!("{} está en perfectas condiciones.", registro.pokemon.nombre),
    EstadoElemento::Envenenado => println!("{} está envenenado.", registro.pokemon.nombre),
    _ => println!("{} tiene un estado especial.", registro.pokemon.nombre),
}

Ejemplo práctico: sistema de batalla simplificado #

Usemos structs y enums para crear un sistema de batalla simple entre dos Pokémon.

Definiendo los Tipos de Movimiento:

enum TipoMovimiento {
    Normal,
    Fuego,
    Agua,
    Eléctrico,
    Planta,
    // Agrega más tipos según sea necesario
}

Actualizando el Enum Movimiento:

enum Movimiento {
    Ataque { nombre: String, poder: u16, tipo: TipoMovimiento },
    Estado { nombre: String, efecto: String },
}

Implementando la Función de Batalla:

fn calcular_dano(ataque: &Pokémon, defensa: &Pokémon, movimiento: &Movimiento) -> u16 {
    match movimiento {
        Movimiento::Ataque { poder, tipo, .. } => {
            // Simplificación: daño = poder * nivel de ataque / nivel de defensa
            let dano = poder * ataque.nivel as u16 / defensa.nivel as u16;
            println!(
                "{} usa {} contra {} y causa {} puntos de daño.",
                ataque.nombre, movimiento.obtener_nombre(), defensa.nombre, dano
            );
            dano
        }
        _ => {
            println!("El movimiento no causa daño directo.");
            0
        }
    }
}

impl Movimiento {
    fn obtener_nombre(&self) -> &str {
        match self {
            Movimiento::Ataque { nombre, .. } => nombre,
            Movimiento::Estado { nombre, .. } => nombre,
        }
    }
}

Simulando una Batalla:

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

let item3 = Pokémon {
    nombre: String::from("Item3"),
    tipo: String::from("Agua"),
    nivel: 20,
};

let impactrueno = Movimiento::Ataque {
    nombre: String::from("Impactrueno"),
    poder: 40,
    tipo: TipoMovimiento::Eléctrico,
};

let dano = calcular_dano(&item1, &item3, &impactrueno);
// Aquí podríamos actualizar la salud de Item3 restando el daño

Salida:

Item1 usa Impactrueno contra Item3 y causa 50 puntos de daño.

Ventajas de usar structs y enums #

  • Organización y Legibilidad: Agrupan datos relacionados, haciendo que el código sea más fácil de entender y mantener.
  • Flexibilidad: Los enums con datos asociados permiten representar diferentes variantes con información adicional.
  • Seguridad en Tiempo de Compilación: Rust verifica que todos los casos de un enum sean manejados, evitando errores en tiempo de ejecución.
  • Modelado de Datos Complejos: Al combinar structs y enums, podemos representar estructuras de datos complejas y relaciones entre ellas.

Conclusión #

Las structs y enums son herramientas esenciales en Rust para organizar y modelar datos de manera eficiente y segura. A través de ejemplos del mundo Pokémon, hemos visto cómo utilizarlas para representar entidades, estados y comportamientos en nuestros programas.

Al dominar estas estructuras, estarás mejor preparado para enfrentar proyectos más complejos y escribir código más robusto y mantenible.


¿Disfrutaste este artículo? ¡Te animo a que experimentes creando tus propias structs y enums para modelar diferentes aspectos del mundo Pokémon o cualquier otro dominio que te apasione!

Si tienes preguntas o quieres compartir tus creaciones, ¡deja un comentario abajo!

Próxima vez en Rustaceo: Exploraremos “Pattern Matching con Enums: Control de Flujo Avanzado en Rust”, continuando nuestra aventura en el fascinante mundo de Rust.

¡Hasta la próxima, entrenadores y entusiastas de Rust!