12 - Pattern matching con `match` e `if let` - control de flujo avanzado en Rust con pokémon

12 - Pattern matching con `match` e `if let` - control de flujo avanzado en Rust con pokémon

¡Bienvenidos de nuevo a Rustaceo.es! En nuestra exploración continua de Rust y sus poderosas características, hoy nos sumergiremos en el pattern matching utilizando match e if let. Estas herramientas nos permiten manejar el control de flujo de manera elegante y eficiente. Utilizaremos ejemplos del mundo Pokémon para ilustrar cómo funcionan y cómo pueden mejorar nuestro código. ¡Acompáñame en esta aventura!

¿Qué es el pattern matching? #

El pattern matching es una característica que permite comparar un valor contra una serie de patrones y ejecutar código basado en el primer patrón coincidente. En Rust, el pattern matching es exhaustivo y seguro, lo que significa que el compilador verifica que todos los casos posibles sean manejados.

La expresión match #

La expresión match es una forma poderosa de manejar múltiples casos basados en el valor de una variable. Funciona de manera similar a una declaración switch en otros lenguajes, pero con muchas más capacidades.

Sintaxis básica de match #

match valor {
    patrón1 => expresión1,
    patrón2 => expresión2,
    _ => expresión_por_defecto,
}
  • valor: La variable o expresión que estamos comparando.
  • patrón: El patrón contra el cual estamos comparando el valor.
  • _: Un comodín que coincide con cualquier valor no cubierto por patrones anteriores.

Usando match con enums #

Los enums son excelentes para usar con match debido a sus múltiples variantes. Veamos cómo funciona con un ejemplo de Pokémon.

Definiendo un enum para tipos de pokémon #

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

Asignando tipos a pokémon #

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

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

Usando match para determinar debilidades #

Queremos determinar contra qué tipo de Pokémon es débil nuestro Pokémon.

fn debilidad(pokemon: &Pokémon) {
    match pokemon.tipo {
        TipoElemento::Fuego => println!("{} es débil contra Agua.", pokemon.nombre),
        TipoElemento::Agua => println!("{} es débil contra Eléctrico y Planta.", pokemon.nombre),
        TipoElemento::Planta => println!("{} es débil contra Fuego y Volador.", pokemon.nombre),
        TipoElemento::Eléctrico => println!("{} es débil contra Tierra.", pokemon.nombre),
        _ => println!("{} tiene debilidades variadas.", pokemon.nombre),
    }
}

debilidad(&item2);

Salida:

Item2 es débil contra Agua.

Manejo de múltiples patrones #

Podemos combinar múltiples patrones utilizando el operador |.

match pokemon.tipo {
    TipoElemento::Fuego | TipoElemento::Eléctrico => println!("{} es fuerte contra Planta.", pokemon.nombre),
    TipoElemento::Agua => println!("{} es fuerte contra Fuego.", pokemon.nombre),
    TipoElemento::Planta => println!("{} es fuerte contra Agua.", pokemon.nombre),
    _ => println!("{} tiene fortalezas variadas.", pokemon.nombre),
}

Desestructuración con match #

Podemos desestructurar structs y enums dentro de match para acceder a sus campos.

Ejemplo con enums que tienen datos asociados #

Definamos un enum para movimientos que incluye datos asociados.

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

Creamos un movimiento:

let ascuas = Movimiento::Ataque {
    nombre: String::from("Ascuas"),
    poder: 40,
    tipo: TipoElemento::Fuego,
};

Usamos match para manejar diferentes variantes:

match ascuas {
    Movimiento::Ataque { nombre, poder, tipo } => {
        println!("Movimiento de ataque: {}, Poder: {}, Tipo: {:?}", nombre, poder, tipo);
    }
    Movimiento::Estado { nombre, efecto } => {
        println!("Movimiento de estado: {}, Efecto: {}", nombre, efecto);
    }
}

Salida:

Movimiento de ataque: Ascuas, Poder: 40, Tipo: Fuego

La expresión if let #

if let es una forma concisa de manejar casos donde solo nos interesa una variante específica de un enum.

Sintaxis de if let #

if let patrón = valor {
    // Código a ejecutar si el patrón coincide
}

Ejemplo con option #

Supongamos que tenemos una variable de tipo Option<Pokémon>.

let posible_elemento: Option<Pokémon> = Some(item2);

if let Some(pokemon) = posible_elemento {
    println!("Has encontrado a {}.", pokemon.nombre);
} else {
    println!("No hay ningún Pokémon aquí.");
}

Salida:

Has encontrado a Item2.

Usando if let para ignorar variantes #

Si solo nos interesa manejar una variante y queremos ignorar las demás, if let es muy útil.

Ejemplo con Resultados de Captura

enum ResultadoCaptura {
    Exito(Pokémon),
    Falla,
}

let resultado = ResultadoCaptura::Exito(item2);

if let ResultadoCaptura::Exito(pokemon) = resultado {
    println!("¡Has capturado a {}!", pokemon.nombre);
}

La expresión while let #

while let nos permite realizar bucles que continúan mientras un patrón coincide.

Ejemplo con iteradores #

Supongamos que tenemos una pila de Poké Balls representada como un vector.

let mut poke_balls = vec!["Poké Ball", "Super Ball", "Ultra Ball"];

while let Some(ball) = poke_balls.pop() {
    println!("Usaste una {}.", ball);
}

Salida:

Usaste una Ultra Ball.
Usaste una Super Ball.
Usaste una Poké Ball.

Pattern matching avanzado #

Ignorando partes de un patrón #

Podemos usar _ para ignorar partes de un patrón que no nos interesan.

match movimiento {
    Movimiento::Ataque { nombre, .. } => {
        println!("Usas el movimiento de ataque {}.", nombre);
    }
    _ => println!("Usas un movimiento no ofensivo."),
}

Guardas en patrones #

Las guardas nos permiten agregar condiciones adicionales en los patrones.

Ejemplo:

match pokemon.nivel {
    nivel if nivel >= 50 => println!("{} es un Pokémon de alto nivel.", pokemon.nombre),
    nivel if nivel >= 20 => println!("{} es un Pokémon de nivel medio.", pokemon.nombre),
    _ => println!("{} es un Pokémon de bajo nivel.", pokemon.nombre),
}

Desestructuración de referencias #

Si trabajamos con referencias, debemos ajustar nuestros patrones.

match &pokemon.tipo {
    TipoElemento::Fuego => println!("{} es de tipo Fuego.", pokemon.nombre),
    _ => println!("{} es de otro tipo.", pokemon.nombre),
}

Ejemplo práctico: sistema de captura de pokémon #

Combina todo lo aprendido para crear un sistema de captura simple.

Definimos el Enum ResultadoCaptura:

enum ResultadoCaptura {
    Exito(Pokémon),
    Falla,
}

Función para Intentar Capturar un Pokémon:

fn intentar_captura(elemento_salvaje: Pokémon) -> ResultadoCaptura {
    // Simulamos una probabilidad de captura
    let probabilidad = 0.5;
    let captura_exitosa = rand::random::<f32>() < probabilidad;

    if captura_exitosa {
        ResultadoCaptura::Exito(elemento_salvaje)
    } else {
        ResultadoCaptura::Falla
    }
}

Usando match para Manejar el Resultado:

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

match intentar_captura(item1_salvaje) {
    ResultadoCaptura::Exito(pokemon) => println!("¡Has capturado a {}!", pokemon.nombre),
    ResultadoCaptura::Falla => println!("El Pokémon escapó."),
}

Conclusión #

El pattern matching con match e if let es una herramienta poderosa que nos permite manejar el control de flujo de manera clara y segura en Rust. Al utilizar estas expresiones, podemos escribir código más legible y eficiente, evitando errores y manejando todos los casos posibles.

Al aplicar estos conceptos en ejemplos del mundo Pokémon, hemos visto cómo se pueden utilizar en situaciones prácticas y entretenidas. Te animo a que experimentes con tus propios ejemplos y descubras todo lo que el pattern matching puede ofrecer.


¿Te ha resultado útil este artículo? ¡Intenta implementar tus propias funciones utilizando match e if let para manejar diferentes escenarios en tus programas de Rust! Si tienes preguntas o quieres compartir tus experiencias, ¡deja un comentario abajo!

Próxima vez en Rustaceo: Exploraremos “Aplicando Generics y Traits: Escribiendo Código Reutilizable en Rust”, continuando nuestra emocionante aventura en el mundo de Rust.

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