rust-pattern-matching-match

Pattern matching en Rust con match y desestructuración

  • 5 min

El pattern matching consiste en comparar un valor con patrones y extraer sus datos internos.

En el artículo anterior vimos cómo guardar datos complejos dentro de un Enum. Nos quedó una duda pendiente: ¿cómo sacamos esos datos?

Si tengo una variable mensaje que puede ser Mover {x, y} o Escribir(texto), ¿cómo accedo a x o a texto? No podemos usar mensaje.x porque si la variable fuera del tipo Escribir, x no existiría y el programa explotaría.

Rust soluciona esto con el pattern matching (coincidencia de patrones) y su herramienta principal: la expresión match.

Podemos pensar en match como una versión mucho más expresiva del switch de C o Java.

La sintaxis básica

Un bloque match toma una expresión y la compara con una serie de patrones. Cuando encuentra uno que “encaja”, ejecuta el código asociado.

Recuperemos nuestro enum simple de direcciones:

enum Sentido {
    Norte,
    Sur,
    Este,
    Oeste,
}

fn destino(direccion: Sentido) {
    match direccion {
        Sentido::Norte => println!("Vamos hacia el frío"),
        Sentido::Sur => println!("Vamos al calorcito"),
        Sentido::Este => println!("Hacia donde sale el sol"),
        Sentido::Oeste => println!("Hacia donde se pone"),
    }
}
Copied!

La sintaxis es Patrón => Código. Si el código es breve, puede ir en una línea con ,. Si es complejo, usamos llaves {}.

Exhaustividad

Con esto, match barre al switch tradicional. En C++, si olvidas un case en un switch, el programa compila y simplemente no hace nada (lo cual suele ser un bug).

En Rust, el match debe ser exhaustivo. Debes cubrir todas y cada una de las posibilidades.

Si comentamos una línea:

match direccion {
    Sentido::Norte => println!("Norte"),
    Sentido::Sur => println!("Sur"),
    // Olvidamos Este y Oeste
}
// ❌ Error de compilación: pattern `Este` not covered
Copied!

El compilador es tu red de seguridad. Te garantiza que nunca tendrás un estado no manejado en tu lógica.

El comodín _

A veces no quieres escribir un caso para cada variante (imagina un enum con 100 opciones, o un número entero). Para eso tenemos el patrón comodín _, que significa “cualquier otra cosa”.

let numero = 7;

match numero {
    1 => println!("Uno"),
    2 => println!("Dos"),
    3 => println!("Tres"),
    _ => println!("Cualquier otro número"), // Obligatorio para ser exhaustivo
}
Copied!

Desestructuración

Esta es la verdadera potencia. El match puede “abrir” el enum y extraer los valores que lleva dentro mediante desestructuración.

Volvamos a nuestro ejemplo del sistema de mensajes:

enum Mensaje {
    Salir,
    Mover { x: i32, y: i32 },
    Escribir(String),
    CambiarColor(i32, i32, i32),
}

fn procesar(msg: Mensaje) {
    match msg {
        Mensaje::Salir => {
            println!("Adiós!");
        },
        Mensaje::Mover { x, y } => {
            // Aquí hemos creado dos variables 'x' e 'y' válidas
            println!("Moviendo a coordenadas: {}, {}", x, y);
        },
        Mensaje::Escribir(texto) => {
            // Extraemos el String a la variable 'texto'
            println!("Mensaje recibido: {}", texto);
        },
        Mensaje::CambiarColor(r, g, b) => {
            println!("Cambiando color a RGB({}, {}, {})", r, g, b);
        }
    }
}
Copied!

Fíjate en la elegancia.

  1. Verificamos qué variante es.
  2. Extraemos los datos internos a variables.
  3. Usamos esas variables. Todo en un solo paso y con seguridad de tipos garantizada.

Matching con Option<T>

Como dijimos, match es la forma estándar de lidiar con Option (el reemplazo de null).

Imagina una función que devuelve un Option<i32>.

fn mas_uno(x: Option<i32>) -> Option<i32> {
    match x {
        None => None, // Si no hay nada, devolvemos nada
        Some(i) => Some(i + 1), // Si hay un número 'i', sumamos 1
    }
}

fn main() {
    let cinco = Some(5);
    let seis = mas_uno(cinco); // Some(6)
    let nada = mas_uno(None);  // None
}
Copied!

Este patrón es tan común que Rust tiene atajos para él, como if let, y métodos como map que expresan la misma idea sin escribir un match completo.

match es una expresión

Al igual que if, match es una expresión. Esto significa que devuelve un valor y podemos asignarlo a una variable.

let booleano = true;

let numero = match booleano {
    true => 1,
    false => 0,
}; // <-- El punto y coma va aquí

println!("El número es {}", numero);
Copied!

Para que esto funcione, todas las ramas (brazos) del match deben devolver el mismo tipo de dato. No puedes devolver un 1 en una rama y un "hola" en otra.

Otros patrones

El sistema de patrones de Rust es muy profundo. Aquí tienes algunos trucos extra:

Varios patrones (|)

Puedes emparejar varias opciones en una sola línea usando el operador OR |.

match x {
    1 | 2 => println!("Uno o Dos"),
    3 => println!("Tres"),
    _ => println!("Otro"),
}
Copied!

Rangos (..=)

Puedes usar rangos en los patrones (especialmente útil con caracteres o números).

let x = 5;
match x {
    1..=5 => println!("Entre 1 y 5"),
    _ => println!("Otro"),
}
Copied!

Guardas (if)

Puedes añadir una condición extra a un patrón.

let par = Some(4);

match par {
    Some(x) if x % 2 == 0 => println!("Es un número par: {}", x),
    Some(x) => println!("Es impar: {}", x),
    None => (),
}
Copied!