wandres.dev
CONTROL DE FLUJO · if, loop, while, match

match: el pattern matching

La introducción al pattern matching de Rust: match como expresión, la exhaustividad obligatoria que el compilador verifica, los patrones (literales, rangos, guardas, enlaces) y por qué es infinitamente más potente que un switch.

⏱ 14 min

Si tuvieras que quedarte con una sola construcción de Rust, muchos elegirían match. Aparenta ser el switch de C, pero es otra especie: no compara igualdad contra constantes, sino que descompone un valor contra patrones, liga partes de él a variables, y —el rasgo decisivo— exige que cubras todos los casos posibles o no compila. Esa exhaustividad convierte “¿habré manejado todas las variantes?” de una duda en tiempo de ejecución a una garantía en tiempo de compilación.

🎯 Al terminar esta lección sabrás
  • Usar match como expresión que devuelve un valor.
  • Entender la exhaustividad obligatoria y por qué es una red de seguridad.
  • Escribir patrones: literales, rangos, or-patterns, guardas y enlaces con @.
  • Ver por qué match supera a cualquier switch.

match como expresión

match compara un valor contra una serie de brazos, cada uno un patrón y un cuerpo separados por =>. Se ejecuta el primero que encaja, y su cuerpo es el valor de todo el match:

let dado = 4;
let texto = match dado {
    1 => "uno",
    2 => "dos",
    3 | 4 | 5 => "en medio",     // or-pattern: varios valores, un brazo
    6 => "seis",
    _ => "imposible en un dado", // comodín: captura el resto
};

Como es una expresión, todos los brazos deben producir el mismo tipo (aquí, &str), igual que las ramas de un if. El comodín _ encaja con cualquier cosa y no liga nombre; es la vía de escape para “todo lo demás”.

La exhaustividad no es opcional

Aquí match se separa del switch. El compilador comprueba que los patrones cubren todos los valores posibles del tipo. Si te dejas uno, es un error, no un aviso:

let n: u8 = 200;
match n {
    0 => println!("cero"),
    1..=255 => println!("resto"),
} // OK: 0 y 1..=255 cubren todos los u8

match n {
    0 => println!("cero"),
    // ERROR E0004: non-exhaustive patterns: `1..=255` not covered
}

Esto brilla con los enum. Imagina un enum Semaforo { Rojo, Ambar, Verde }. Un match sobre él debe tratar las tres variantes. Y si mañana alguien añade una cuarta, Semaforo::Intermitente, todos los match que no la contemplen dejarán de compilar, señalándote exactamente dónde has de decidir qué hacer con el caso nuevo:

enum Semaforo { Rojo, Ambar, Verde }

fn accion(s: Semaforo) -> &'static str {
    match s {
        Semaforo::Rojo => "parar",
        Semaforo::Ambar => "precaucion",
        Semaforo::Verde => "avanzar",
    } // exhaustivo: quita un brazo y no compila
}

La verdadera potencia llega cuando las variantes llevan datos: match los extrae en el mismo gesto con que distingue la variante, algo que ningún switch puede hacer.

enum Evento {
    Clic { x: i64, y: i64 },
    Tecla(char),
    Cerrar,
}

fn describir(e: Evento) -> String {
    match e {
        Evento::Clic { x, y } => format!("clic en {x},{y}"),
        Evento::Tecla(c) => format!("tecla {c}"),
        Evento::Cerrar => "cerrar".to_string(),
    }
}
⚠️
El comodín _ es poder y es peligro

_ (o un nombre ligado como otro) vuelve un match exhaustivo al instante, pero a un precio: renuncia a la verificación futura. Si usas _ => ... sobre un enum y luego añades una variante, el match la absorberá en silencio por el comodín en lugar de avisarte. Usa _ para tipos con infinitos valores (enteros, cadenas) donde enumerar es imposible; sobre enum cerrados, prefiere listar cada variante para que el compilador siga siendo tu revisor cuando el tipo evolucione.

Patrones: un sublenguaje entero

Lo que va a la izquierda de => no es un valor: es un patrón, y los patrones forman un pequeño lenguaje propio. Pueden descomponer estructuras, ligar partes a variables, filtrar por rango y añadir condiciones extra:

let punto = (0, -7);
let donde = match punto {
    (0, 0) => "origen".to_string(),
    (x, 0) => format!("eje X en {x}"),          // liga x, exige y == 0
    (0, y) => format!("eje Y en {y}"),          // liga y, exige x == 0
    (x, y) if x == y => "diagonal".to_string(), // GUARDA: condición extra
    (x, y) => format!("libre en {x},{y}"),      // liga ambos
};

Los ingredientes que acabas de ver:

🎯

Enlaces

(x, y) liga cada componente a un nombre usable en el cuerpo. El patrón desestructura y captura a la vez.

📏

Rangos

1..=5 encaja con un intervalo. Ideal para clasificar números o rangos de caracteres ('a'..='z').

🛡️

Guardas

patrón if cond añade una condición booleana arbitraria que el patrón por sí solo no puede expresar.

🔗

Enlace con @

id @ 3..=7 comprueba el rango y a la vez liga el valor concreto a id para usarlo en el cuerpo.

El enlace con @ merece un ejemplo, porque une comprobar y capturar en un solo gesto:

let edad = 25;
match edad {
    n @ 0..=17 => println!("menor, {n} años"),
    n @ 18..=64 => println!("adulto, {n} años"),
    n => println!("mayor, {n} años"),
}

Un último detalle ergonómico: al hacer match sobre una referencia, Rust ajusta los enlaces para que presten en vez de mover. Matchear &Some(x) liga x como &T sin que tengas que escribir ref a mano.

let texto = Some(String::from("hola"));
match &texto {
    Some(s) => println!("{} letras", s.len()), // s: &String, no mueve
    None => {}
}
// `texto` sigue disponible aquí: el match solo prestó
La exhaustividad convierte el sistema de tipos en un demostrador de teoremas

Un switch de C es un atajo para una cadena de if: solo compara igualdad contra constantes enteras, se le olvida el break y cae de un caso a otro, y no le importa lo más mínimo si dejaste casos sin tratar. El match de Rust invierte la relación de poder entre tú y el compilador. Cuando escribes un match sobre un enum, estás afirmando implícitamente “he considerado todas las formas que este valor puede tomar”, y el compilador verifica esa afirmación con un algoritmo de exhaustividad y de utilidad que además detecta brazos inalcanzables. La consecuencia práctica es enorme: modela tus estados como un enum y el compilador se vuelve un asistente que te obliga a manejar cada uno. Añade un estado nuevo y no tienes que buscar a mano por todo el código dónde atenderlo: la compilación falla en cada match afectado y te lleva de la mano hasta él. Esto es refactorización guiada por el sistema de tipos, y es la base de la máxima “haz que los estados imposibles sean irrepresentables”. El pattern matching no es una comodidad sintáctica: es el mecanismo por el que Rust transforma decisiones de diseño en invariantes que la máquina verifica por ti. Cuando en el nivel de los enum y Option combines esto con datos dentro de las variantes, verás que buena parte de la corrección de un programa Rust se demuestra sola en compilación.

⚔️ Piensa en patrones
  1. Escribe un match sobre un u8 que clasifique con rangos (0, 1..=9, 10..=99, _) y devuelva un &str; observa qué pasa si borras el _.
  2. Define enum Direccion { Norte, Sur, Este, Oeste } y un match exhaustivo sobre él; añade una quinta variante y comprueba que el match deja de compilar.
  3. Desestructura una tupla (i32, i32) distinguiendo origen, ejes y diagonal con una guarda if x == y.
  4. Usa un enlace n @ 100..=200 para, en un solo brazo, comprobar el rango y a la vez imprimir el valor concreto.