Rust

Rust

Tutorial Rust: Pattern Matching

Aprende pattern matching exhaustivo, destructuring y uso de if let y while let en Rust para manejar datos complejos de forma segura y concisa.

Aprende Rust y certifícate

match exhaustivo

El pattern matching (coincidencia de patrones) es una de las características más potentes de Rust, permitiéndonos examinar y descomponer valores complejos de manera segura y expresiva. En el centro de esta funcionalidad se encuentra la expresión match, que nos permite comparar un valor contra una serie de patrones y ejecutar código basado en qué patrón coincide.

La expresión match en Rust destaca por una propiedad fundamental: la exhaustividad. Esto significa que Rust verifica en tiempo de compilación que todos los posibles casos sean manejados, evitando errores en tiempo de ejecución por casos no contemplados.

Sintaxis básica de match

La estructura básica de una expresión match es la siguiente:

match valor_a_comparar {
    patrón1 => expresión1,
    patrón2 => expresión2,
    patrón3 => { 
        // Bloque de código para casos más complejos
        let resultado = hacer_algo();
        resultado
    },
    // Más patrones...
}

Cada rama de un match consiste en un patrón seguido de => y la expresión o bloque de código a ejecutar cuando ese patrón coincide. El valor resultante de la expresión match será el valor de la rama que coincida.

Exhaustividad: la seguridad de no olvidar casos

La característica más importante de match es que el compilador de Rust verifica que todos los posibles valores sean manejados. Esto previene errores comunes como olvidar manejar un caso específico.

Veamos un ejemplo con un enum simple:

enum Semaforo {
    Rojo,
    Amarillo,
    Verde,
}

fn accion_segun_semaforo(color: Semaforo) {
    match color {
        Semaforo::Rojo => println!("¡Detente!"),
        Semaforo::Verde => println!("Adelante"),
        // Si omitimos el caso Amarillo, el compilador mostrará un error
    }
}

Si intentamos compilar este código, Rust nos mostrará un error indicando que no hemos manejado todos los casos posibles:

error[E0004]: non-exhaustive patterns: `Semaforo::Amarillo` not covered

Para corregirlo, debemos incluir todos los casos:

fn accion_segun_semaforo(color: Semaforo) {
    match color {
        Semaforo::Rojo => println!("¡Detente!"),
        Semaforo::Amarillo => println!("¡Precaución!"),
        Semaforo::Verde => println!("Adelante"),
    }
}

El patrón comodín (_)

En situaciones donde tenemos muchos casos posibles y solo nos interesan algunos específicos, podemos usar el patrón comodín _ para manejar todos los casos restantes:

enum Moneda {
    Euro,
    Dolar,
    Libra,
    Yen,
    Bitcoin,
}

fn valor_en_euros(moneda: Moneda) -> f64 {
    match moneda {
        Moneda::Euro => 1.0,
        Moneda::Dolar => 0.85,
        Moneda::Libra => 1.17,
        _ => {
            println!("Conversión aproximada para otras monedas");
            0.5 // Valor por defecto para otras monedas
        }
    }
}

El patrón _ captura todos los casos que no fueron manejados explícitamente, manteniendo la exhaustividad del match.

Match con valores literales

También podemos hacer coincidir valores literales como números o caracteres:

fn describir_numero(n: i32) -> &'static str {
    match n {
        0 => "cero",
        1 => "uno",
        2 => "dos",
        _ if n < 0 => "negativo",
        _ => "otro número positivo",
    }
}

Observa cómo usamos una condición de guarda (if n < 0) para refinar aún más nuestros patrones.

Patrones con rangos

Rust permite usar rangos en los patrones para simplificar el código:

fn calificar_examen(puntuacion: u8) -> &'static str {
    match puntuacion {
        0..=49 => "Suspenso",
        50..=69 => "Aprobado",
        70..=89 => "Notable",
        90..=100 => "Sobresaliente",
        _ => "Puntuación inválida",
    }
}

El operador ..= crea un rango inclusivo, permitiéndonos manejar grupos de valores de forma concisa.

Match con enums que contienen datos

El verdadero poder de match se aprecia cuando trabajamos con enums que contienen datos:

enum Contenido {
    Texto(String),
    Numero(i32),
    Lista(Vec<String>),
    Nada,
}

fn procesar_contenido(contenido: Contenido) {
    match contenido {
        Contenido::Texto(texto) => {
            println!("Contenido de texto: {}", texto);
        }
        Contenido::Numero(n) if n > 0 => {
            println!("Número positivo: {}", n);
        }
        Contenido::Numero(n) => {
            println!("Número no positivo: {}", n);
        }
        Contenido::Lista(items) if items.is_empty() => {
            println!("Lista vacía");
        }
        Contenido::Lista(items) => {
            println!("Lista con {} elementos", items.len());
        }
        Contenido::Nada => {
            println!("Sin contenido");
        }
    }
}

En este ejemplo, no solo verificamos qué variante del enum tenemos, sino que también extraemos y utilizamos los datos contenidos en cada variante.

Múltiples patrones con el operador |

Podemos combinar varios patrones en una sola rama usando el operador | (OR):

fn es_vocal(c: char) -> bool {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' | 
        'A' | 'E' | 'I' | 'O' | 'U' => true,
        _ => false,
    }
}

Capturando el valor en el patrón comodín

Si necesitamos usar el valor capturado por el patrón comodín, podemos darle un nombre:

enum Mensaje {
    Saludo(String),
    Despedida,
    Personalizado { contenido: String, urgente: bool },
}

fn procesar_mensaje(msg: Mensaje) {
    match msg {
        Mensaje::Saludo(nombre) => println!("¡Hola, {}!", nombre),
        Mensaje::Despedida => println!("¡Hasta luego!"),
        otro_mensaje => {
            println!("Procesando otro tipo de mensaje");
            // Podemos usar 'otro_mensaje' aquí
        }
    }
}

Beneficios de la exhaustividad

La exhaustividad de match proporciona varios beneficios importantes:

  • Seguridad: El compilador garantiza que todos los casos posibles sean manejados.
  • Mantenibilidad: Si añadimos una nueva variante a un enum, el compilador nos indicará todos los lugares donde debemos actualizar nuestro código.
  • Documentación implícita: El código muestra claramente todos los casos que se están considerando.
// Si añadimos una nueva variante al enum Semaforo:
enum Semaforo {
    Rojo,
    Amarillo,
    Verde,
    Intermitente,  // Nueva variante
}

// El compilador nos obligará a actualizar todas las expresiones match
// que usen este enum, evitando errores por casos no contemplados

Patrones @ para vincular valores

El operador @ nos permite vincular un valor a un nombre mientras verificamos que coincida con un patrón:

fn describir_numero_en_rango(x: i32) -> &'static str {
    match x {
        n @ 0..=9 => {
            println!("Tenemos un dígito: {}", n);
            "dígito"
        }
        n @ 10..=99 => {
            println!("Tenemos un número de dos cifras: {}", n);
            "dos cifras"
        }
        n => {
            println!("Tenemos un número grande: {}", n);
            "grande"
        }
    }
}

El patrón match exhaustivo es la base del sistema de pattern matching en Rust, proporcionando una forma segura y expresiva de trabajar con datos complejos. En las siguientes secciones, veremos cómo podemos aplicar estos conceptos para desestructurar datos y utilizar formas más concisas de pattern matching con if let y while let.

Destructuring

El destructuring (desestructuración) es una técnica elegante que nos permite extraer y descomponer valores de estructuras de datos complejas en Rust. Esta característica nos permite acceder a partes específicas de structs, enums, tuplas y arrays de forma concisa y expresiva.

La desestructuración funciona como un espejo de la construcción: si sabemos cómo se estructura un valor, podemos desarmarlo siguiendo ese mismo patrón. Esto resulta particularmente útil cuando solo necesitamos trabajar con algunas partes de una estructura de datos compleja.

Desestructuración de tuplas

Las tuplas son una de las estructuras más simples que podemos desestructurar:

fn main() {
    let punto = (10, 20);
    
    // Desestructuración básica
    let (x, y) = punto;
    println!("Coordenadas: x={}, y={}", x, y);
    
    // También funciona con diferentes tipos
    let info = ("Rust", 2015, true);
    let (nombre, año, estable) = info;
    println!("{} fue lanzado en {} y estable={}", nombre, año, estable);
}

Podemos ignorar elementos específicos usando el patrón comodín _:

let (x, _, z) = (1, 2, 3);  // Ignoramos el valor del medio
println!("x={}, z={}", x, z);

Desestructuración de structs

La desestructuración de structs nos permite extraer campos específicos de manera elegante:

struct Persona {
    nombre: String,
    edad: u32,
    ciudad: String,
}

fn main() {
    let usuario = Persona {
        nombre: String::from("Ana"),
        edad: 28,
        ciudad: String::from("Madrid"),
    };
    
    // Extraemos solo los campos que necesitamos
    let Persona { nombre, edad, .. } = usuario;
    println!("{} tiene {} años", nombre, edad);
    
    // También podemos asignar a variables con nombres diferentes
    let Persona { nombre: n, edad: e, ciudad: c } = usuario;
    println!("Nombre: {}, Edad: {}, Ciudad: {}", n, e, c);
}

El operador .. nos permite ignorar los campos restantes que no nos interesan, lo que resulta útil en structs con muchos campos.

Desestructuración de enums

La desestructuración de enums es donde realmente brilla esta característica, permitiéndonos extraer los datos contenidos en las variantes:

enum Mensaje {
    Texto(String),
    Coordenadas(i32, i32),
    Configuracion { id: u32, visible: bool },
    Vacio,
}

fn procesar_mensaje(msg: Mensaje) {
    match msg {
        Mensaje::Texto(contenido) => {
            println!("Mensaje de texto: {}", contenido);
        },
        Mensaje::Coordenadas(x, y) => {
            println!("Ubicación: ({}, {})", x, y);
        },
        Mensaje::Configuracion { id, visible } => {
            println!("Configuración #{}: visible={}", id, visible);
        },
        Mensaje::Vacio => {
            println!("No hay contenido");
        },
    }
}

Observa cómo la sintaxis de desestructuración se adapta a cada variante del enum, ya sea que contenga una tupla, múltiples valores o un struct anónimo.

Desestructuración anidada

Podemos combinar patrones para desestructurar estructuras anidadas:

struct Punto {
    x: i32,
    y: i32,
}

enum Figura {
    Circulo { centro: Punto, radio: f64 },
    Rectangulo { esquina_sup_izq: Punto, esquina_inf_der: Punto },
}

fn describir_figura(figura: Figura) {
    match figura {
        Figura::Circulo { 
            centro: Punto { x, y }, 
            radio 
        } => {
            println!("Círculo en ({}, {}) con radio {}", x, y, radio);
        },
        Figura::Rectangulo { 
            esquina_sup_izq: Punto { x: x1, y: y1 }, 
            esquina_inf_der: Punto { x: x2, y: y2 } 
        } => {
            println!("Rectángulo desde ({}, {}) hasta ({}, {})", x1, y1, x2, y2);
        },
    }
}

Este patrón es extremadamente útil cuando trabajamos con estructuras de datos complejas y anidadas, permitiéndonos acceder directamente a los valores más profundos.

Desestructuración en parámetros de funciones

Podemos aplicar la desestructuración directamente en los parámetros de funciones:

struct Config {
    max_conexiones: u32,
    timeout: u64,
    debug: bool,
}

// Desestructuración en la firma de la función
fn iniciar_servidor(Config { max_conexiones, timeout, debug }: Config) {
    println!("Iniciando servidor con:");
    println!("  Max conexiones: {}", max_conexiones);
    println!("  Timeout: {} ms", timeout);
    println!("  Modo debug: {}", debug);
}

// Uso:
let config = Config {
    max_conexiones: 100,
    timeout: 30000,
    debug: true,
};
iniciar_servidor(config);

Esta técnica hace que el código sea más legible al mostrar claramente qué partes de la estructura se utilizan dentro de la función.

Desestructuración parcial con ref y mut

Podemos usar las palabras clave ref y mut para controlar cómo se vinculan los valores durante la desestructuración:

struct Datos {
    valor: String,
    contador: u32,
}

fn main() {
    let datos = Datos {
        valor: String::from("ejemplo"),
        contador: 5,
    };
    
    // 'ref' para obtener referencias en lugar de mover valores
    let Datos { ref valor, contador } = datos;
    println!("Valor: {}, Contador: {}", valor, contador);
    // 'valor' es &String, 'contador' es u32
    
    // Combinando 'ref' y 'mut'
    let mut datos_mut = datos;
    let Datos { ref mut valor, contador: c } = datos_mut;
    valor.push_str(" modificado");
    println!("Nuevo valor: {}, Contador: {}", valor, c);
}

Esto es especialmente útil cuando queremos evitar mover valores o cuando necesitamos modificarlos durante la desestructuración.

Desestructuración de slices y arrays

También podemos desestructurar arrays y slices:

fn main() {
    let numeros = [1, 2, 3, 4, 5];
    
    // Extraer elementos específicos
    let [primero, segundo, .., ultimo] = numeros;
    println!("Primero: {}, Segundo: {}, Último: {}", primero, segundo, ultimo);
    
    // Capturar un slice del medio
    let [cabeza, medio @ .., cola] = numeros;
    println!("Cabeza: {}, Cola: {}, Medio: {:?}", cabeza, cola, medio);
}

El operador @ nos permite vincular un nombre a un subpatrón, lo que resulta útil para capturar slices dentro de arrays.

Patrones de desestructuración en bucles for

La desestructuración también funciona en los bucles for, lo que facilita trabajar con colecciones de tuplas o estructuras:

fn main() {
    let puntos = vec![(0, 0), (1, 5), (10, -3)];
    
    // Desestructuración en el bucle for
    for (x, y) in puntos {
        println!("Punto en ({}, {}), distancia al origen: {:.2}", 
                 x, y, ((x*x + y*y) as f64).sqrt());
    }
    
    // Con estructuras
    let usuarios = vec![
        Persona { nombre: String::from("Carlos"), edad: 32, ciudad: String::from("Barcelona") },
        Persona { nombre: String::from("Elena"), edad: 25, ciudad: String::from("Valencia") },
    ];
    
    for Persona { nombre, edad, .. } in usuarios {
        println!("{} tiene {} años", nombre, edad);
    }
}

Esta técnica hace que el código sea más limpio y expresivo al evitar accesos repetitivos a los campos o elementos.

Desestructuración con patrones OR

Podemos combinar la desestructuración con el operador | para manejar múltiples patrones:

enum Dispositivo {
    Movil { marca: String, modelo: String, año: u32 },
    Tablet { marca: String, tamaño: f32 },
    Laptop { marca: String, ram: u32 },
}

fn es_apple(dispositivo: &Dispositivo) -> bool {
    match dispositivo {
        Dispositivo::Movil { marca, .. } | 
        Dispositivo::Tablet { marca, .. } | 
        Dispositivo::Laptop { marca, .. } if marca == "Apple" => true,
        _ => false,
    }
}

Esta técnica nos permite aplicar la misma lógica a diferentes variantes que comparten un campo común.

La desestructuración es una herramienta fundamental en Rust que nos permite escribir código más conciso, expresivo y seguro. Al dominar estos patrones, podremos trabajar con estructuras de datos complejas de manera elegante y sin sacrificar la seguridad que caracteriza a Rust.

if let y while let

Mientras que la expresión match es extremadamente poderosa para el pattern matching exhaustivo, a veces resulta demasiado verbosa cuando solo nos interesa un patrón específico. Rust ofrece construcciones más concisas para estos casos: if let y while let, que nos permiten trabajar con patrones de forma más elegante cuando solo nos importa un caso particular.

La sintaxis if let

La construcción if let nos permite combinar un if con un patrón, ejecutando código solo cuando el valor coincide con dicho patrón:

if let Patrón = expresión {
    // Código que se ejecuta cuando el patrón coincide
} else {
    // Código opcional que se ejecuta cuando no coincide
}

Esta sintaxis es particularmente útil cuando trabajamos con enums y solo nos interesa una de sus variantes.

Comparación entre match e if let

Veamos un ejemplo comparativo para entender mejor cuándo usar if let:

enum Temperatura {
    Celsius(i32),
    Fahrenheit(i32),
    Kelvin(i32),
}

// Usando match (más verboso)
fn mostrar_celsius_match(temp: Temperatura) {
    match temp {
        Temperatura::Celsius(grados) => {
            println!("La temperatura es {}°C", grados);
        },
        _ => {
            // No hacemos nada con las otras variantes
        }
    }
}

// Usando if let (más conciso)
fn mostrar_celsius_if_let(temp: Temperatura) {
    if let Temperatura::Celsius(grados) = temp {
        println!("La temperatura es {}°C", grados);
    }
}

Como podemos observar, if let elimina la necesidad de escribir el caso comodín _ cuando solo nos interesa un patrón específico.

Combinando if let con else

Podemos extender if let con ramas else para manejar el caso cuando el patrón no coincide:

enum Estado {
    Conectado { ip: String, puerto: u16 },
    Desconectado,
    EnEspera,
}

fn verificar_conexion(estado: Estado) {
    if let Estado::Conectado { ip, puerto } = estado {
        println!("Conectado a {}:{}", ip, puerto);
    } else {
        println!("No está conectado actualmente");
    }
}

También podemos encadenar múltiples patrones usando else if let:

fn describir_estado(estado: Estado) {
    if let Estado::Conectado { ip, puerto } = estado {
        println!("Conectado a {}:{}", ip, puerto);
    } else if let Estado::EnEspera = estado {
        println!("Conexión en espera, intentando reconectar...");
    } else {
        println!("Desconectado");
    }
}

Patrones complejos con if let

La sintaxis if let admite los mismos patrones complejos que podemos usar en match:

struct Punto {
    x: i32,
    y: i32,
}

enum Forma {
    Circulo(Punto, i32),  // Centro y radio
    Rectangulo(Punto, Punto),  // Esquinas opuestas
}

fn es_circulo_grande(forma: &Forma) {
    if let Forma::Circulo(Punto { x, y }, radio) = forma {
        if *radio > 100 {
            println!("Círculo grande en ({}, {}) con radio {}", x, y, radio);
        }
    }
}

La sintaxis while let

Mientras que if let nos permite ejecutar código una vez si un patrón coincide, while let nos permite repetir código mientras un patrón siga coincidiendo. Esta construcción es especialmente útil para procesar secuencias de valores:

while let Patrón = expresión {
    // Código que se ejecuta mientras el patrón coincida
}

Ejemplo básico de while let

Un caso de uso común es procesar elementos de una pila hasta que esté vacía:

fn procesar_pila() {
    let mut pila = Vec::new();
    pila.push(1);
    pila.push(2);
    pila.push(3);
    
    // Procesamos elementos hasta que la pila esté vacía
    while let Some(valor) = pila.pop() {
        println!("Procesando valor: {}", valor);
    }
}

En este ejemplo, pila.pop() devuelve Some(valor) mientras la pila tenga elementos, y None cuando está vacía. El bucle while let continúa ejecutándose hasta que el patrón deja de coincidir (cuando obtenemos None).

Iterando sobre elementos con índices

Otro uso común de while let es para iterar sobre elementos con sus índices:

fn iterar_con_indices() {
    let numeros = vec![10, 20, 30, 40];
    let mut iter = numeros.iter().enumerate();
    
    while let Some((indice, valor)) = iter.next() {
        println!("Índice: {}, Valor: {}", indice, valor);
    }
}

Aquí estamos desestructurando una tupla (indice, valor) en cada iteración.

Procesando flujos de datos

while let es ideal para procesar flujos de datos donde no conocemos de antemano cuántos elementos procesaremos:

use std::io::{self, BufRead};

fn leer_lineas_no_vacias() {
    let stdin = io::stdin();
    let mut lineas = stdin.lock().lines();
    
    println!("Escribe texto (línea vacía para terminar):");
    
    while let Some(Ok(linea)) = lineas.next() {
        if linea.is_empty() {
            break;
        }
        println!("Leído: {}", linea);
    }
}

Este código lee líneas de la entrada estándar hasta encontrar una línea vacía, demostrando cómo while let puede manejar patrones anidados (Some(Ok(linea))).

Combinando if let y while let

Podemos combinar ambas construcciones para crear lógicas más complejas:

enum Evento {
    Mensaje(String),
    Movimiento { x: i32, y: i32 },
    Tecla(char),
    Salir,
}

fn procesar_eventos(eventos: Vec<Evento>) {
    let mut iter = eventos.into_iter();
    
    while let Some(evento) = iter.next() {
        if let Evento::Mensaje(texto) = evento {
            if texto == "pausa" {
                println!("Pausando procesamiento...");
                // Hacemos algo para pausar
                continue;
            }
            println!("Mensaje: {}", texto);
        } else if let Evento::Salir = evento {
            println!("Saliendo del bucle");
            break;
        } else {
            println!("Otro tipo de evento");
        }
    }
}

Ventajas y desventajas frente a match

Las construcciones if let y while let ofrecen varias ventajas:

  • Concisión: Menos código cuando solo nos interesa un patrón específico
  • Claridad: Expresan claramente la intención de verificar un solo caso
  • Flexibilidad: Se integran bien con otras estructuras de control

Sin embargo, también tienen limitaciones:

  • No exhaustivas: A diferencia de match, el compilador no verifica que hayamos manejado todos los casos posibles
  • Menos visibles: Los patrones que estamos manejando no están tan explícitamente documentados como en un match
// Ejemplo donde if let podría ocultar casos no manejados
enum Resultado {
    Exito(i32),
    Error(String),
    EnProceso,
}

fn procesar_resultado(res: Resultado) {
    // Si solo usamos if let, no es obvio que estamos ignorando el caso EnProceso
    if let Resultado::Exito(valor) = res {
        println!("Éxito con valor: {}", valor);
    } else if let Resultado::Error(msg) = res {
        println!("Error: {}", msg);
    }
    
    // Con match, es más claro que estamos manejando todos los casos
    // o explícitamente ignorando algunos
    /*
    match res {
        Resultado::Exito(valor) => println!("Éxito con valor: {}", valor),
        Resultado::Error(msg) => println!("Error: {}", msg),
        Resultado::EnProceso => (), // Explícitamente no hacemos nada
    }
    */
}

Cuándo usar cada construcción

Como regla general:

  • Usa match cuando:
  • Necesitas manejar múltiples patrones diferentes
  • Quieres asegurarte de manejar todos los casos posibles (exhaustividad)
  • La lógica para cada patrón es simple
  • Usa if let cuando:
  • Solo te interesa un patrón específico
  • Quieres combinar la coincidencia de patrones con otras condiciones
  • Los otros casos pueden manejarse de manera genérica o ignorarse
  • Usa while let cuando:
  • Necesitas procesar elementos mientras un patrón siga coincidiendo
  • Estás trabajando con iteradores o flujos de datos
  • No conoces de antemano cuántos elementos procesarás

Aplicaciones prácticas

Veamos un ejemplo más completo que muestra cómo estas construcciones pueden simplificar el código en situaciones reales:

enum Comando {
    Mover { x: i32, y: i32 },
    Cambiar { color: String },
    Dibujar { forma: String },
    Guardar,
    Salir,
}

fn ejecutar_comandos(comandos: Vec<Comando>) {
    let mut iter = comandos.into_iter();
    
    while let Some(cmd) = iter.next() {
        // Procesamos solo los comandos de movimiento y dibujo
        if let Comando::Mover { x, y } = cmd {
            println!("Moviendo cursor a la posición ({}, {})", x, y);
        } else if let Comando::Dibujar { forma } = cmd {
            println!("Dibujando: {}", forma);
        } else if let Comando::Salir = cmd {
            println!("Saliendo del programa");
            break;
        }
        // Ignoramos otros comandos por ahora
    }
}

Este patrón es común en procesadores de comandos, parsers y sistemas de eventos donde solo nos interesa manejar ciertos tipos de mensajes.

Las construcciones if let y while let complementan perfectamente el sistema de pattern matching de Rust, ofreciendo alternativas más concisas cuando no necesitamos la exhaustividad de match. En la próxima lección, veremos cómo estos conceptos se aplican a Option y Result para implementar un manejo de errores elegante y seguro.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Pattern Matching con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la expresión match y su propiedad de exhaustividad en Rust.
  • Aprender a desestructurar tuplas, structs y enums para extraer datos.
  • Utilizar patrones avanzados como rangos, condiciones de guarda y el operador @.
  • Diferenciar y aplicar las construcciones if let y while let para casos específicos y bucles.
  • Reconocer las ventajas y limitaciones de match frente a if let y while let en el manejo de patrones.