wandres.dev
ENUMS Y PATTERN MATCHING · el superpoder de Rust

Los patrones a fondo: binding, guardas, rangos y desestructuración

El lenguaje completo de patrones de Rust: ligar con arroba, filtrar con guardas, cubrir rangos, combinar con or-patterns, desestructurar structs y tuplas, y el arte de ignorar con guion bajo y dos puntos. La gramática que hace de match una herramienta quirúrgica.

⏱ 18 min

Hasta ahora match abría una variante y sacaba sus datos. Pero el patrón es un mini-lenguaje por derecho propio, capaz de expresar condiciones que en otros lenguajes serían una maraña de if anidados. Ligar un valor y a la vez inspeccionarlo, filtrar con una condición, cubrir un rango, aceptar varias formas a la vez, descender por estructuras anidadas: todo cabe en la parte izquierda de una flecha. Dominar esta gramática es lo que convierte a match de un switch glorificado en un bisturí.

🎯 Al terminar esta lección sabrás
  • Ligar con nombre @ patrón y filtrar ramas con guardas if.
  • Usar patrones de rango ..= y or-patterns |.
  • Desestructurar structs, tuplas y valores anidados en un solo patrón.
  • Distinguir _ de .. e ignorar con precisión.

Binding con @ y guardas con if

Un patrón normal, o comprueba la forma, o liga el valor a un nombre. El operador @ te deja hacer las dos cosas a la vez: liga el valor completo a un nombre y verifica que encaja en un subpatrón.

let n = 7;
match n {
    x @ 1..=9 => println!("dígito {x}"),        // liga x Y comprueba el rango
    x @ 10..=99 => println!("dos cifras: {x}"),
    x => println!("grande: {x}"),
}

Sin @ tendrías que elegir: o compruebas el rango (1..=9 =>) y pierdes el valor, o ligas el valor (x =>) y pierdes la comprobación. El @ te da ambos. Las guardas añaden una condición arbitraria después de que el patrón encaje, con acceso a las variables recién ligadas:

match punto {
    (x, y) if x == y => println!("en la diagonal"),
    (x, y) if x + y == 0 => println!("en la antidiagonal"),
    (x, y) => println!("libre en ({x}, {y})"),
}

Cuidado con la exhaustividad: el compilador no razona sobre el contenido de una guarda, así que un match cuyas únicas ramas llevan guarda sigue necesitando un caso final sin guarda que lo cubra todo. La guarda expresa lógica que el sistema de tipos no puede verificar por ti.

📝
La precedencia de @ y las or-patterns dentro

Desde Rust 2021 y consolidado en 2024, puedes combinar @ con or-patterns anidadas: x @ (1 | 2 | 3) liga x si el valor es cualquiera de los tres. El patrón a la derecha del @ es un patrón completo, no solo un literal, y esa composición es lo que da a la gramática su potencia real.

Rangos y or-patterns

El patrón de rango ..= (inclusivo) cubre un tramo contiguo de valores ordenados: enteros o caracteres. El or-pattern | acepta un valor que encaje con cualquiera de varios patrones, y ambos se combinan:

fn clasifica(c: char) -> &'static str {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' => "vocal",
        'a'..='z' => "consonante",
        '0'..='9' => "dígito",
        ' ' | '\t' | '\n' => "espacio",
        _ => "otro",
    }
}

Las or-patterns funcionan también dentro de una variante, no solo con literales. Y hay una regla de oro: todos los subpatrones de un | deben ligar exactamente las mismas variables con los mismos tipos, porque la rama de la derecha debe funcionar sea cual sea el subpatrón que encajó:

enum Evento {
    Click { x: i32, y: i32 },
    Drag { x: i32, y: i32 },
    Tecla(char),
}

fn coordenada(e: &Evento) -> Option<(i32, i32)> {
    match e {
        // ambos subpatrones ligan x e y: valido
        Evento::Click { x, y } | Evento::Drag { x, y } => Some((*x, *y)),
        Evento::Tecla(_) => None,
    }
}

Desestructurar structs, tuplas y lo anidado

Un patrón puede descender por toda una estructura de datos en un solo golpe, ligando lo que te interesa en el camino. Esto es lo que ahorra las cascadas de accesos a.b.c:

struct Punto { x: i32, y: i32 }
struct Linea { desde: Punto, hasta: Punto }

let linea = Linea {
    desde: Punto { x: 0, y: 0 },
    hasta: Punto { x: 3, y: 4 },
};

// desestructuracion anidada: bajamos hasta las coordenadas de golpe
let Linea { desde: Punto { x: x0, y: y0 }, hasta: Punto { x: x1, y: y1 } } = linea;
println!("de ({x0},{y0}) a ({x1},{y1})");

Este último es un patrón irrefutable: siempre encaja, porque Linea tiene una sola forma, así que sirve directamente en un let. Los patrones de un enum son refutables (pueden fallar), y por eso viven en match, if let o let ... else. La distinción no es un tecnicismo: el compilador rechaza un patrón refutable en un let normal y un patrón irrefutable en un if let (sería siempre verdadero). Nota además el manejo de referencias: al desestructurar &Evento en la sección anterior, x e y se ligaron como &i32 gracias a los binding modes por defecto (las match ergonomics), que insertan las referencias necesarias para que no tengas que escribir ref a mano.

Ignorar con precisión: _ frente a ..

Ignorar bien es tan importante como capturar bien, y hay dos herramientas que no son intercambiables:

  • _ ignora un valor, exactamente uno, y no lo liga (no consume, no dispara el aviso de variable sin usar).
  • .. ignora el resto de los campos o elementos, cuantos sean, en un struct o una tupla o un slice.
struct Config { host: String, port: u16, timeout: u32, reintentos: u8 }

let cfg = Config { host: "localhost".into(), port: 8080, timeout: 30, reintentos: 3 };

// solo me interesa el puerto; el resto, ignorado con ..
let Config { port, .. } = &cfg;
println!("puerto {port}");

// en tuplas y slices: primero y ultimo, ignorando el centro
let numeros = [1, 2, 3, 4, 5];
match numeros {
    [primero, .., ultimo] => println!("bordes: {primero} y {ultimo}"),
}

.. solo puede aparecer una vez por patrón (si no, sería ambiguo qué elemento es cuál) y hace tu código robusto ante cambios: añadir un campo al struct Config no rompe el patrón { port, .. }, mientras que enumerar campos con _ sí te obligaría a añadir otro _. Elige .. cuando quieras decir “y lo demás me da igual, ahora y en el futuro”.

Todo junto: la potencia compuesta

Estas piezas no viven aisladas: se combinan en un solo patrón que liga, filtra, cubre rango y desestructura a la vez. Un brazo así reemplaza lo que en otro lenguaje serían cinco líneas de if anidados:

match evento {
    // desestructura, liga con @, comprueba rango y filtra con guarda, todo junto
    Evento::Click { x: x @ 0..=1920, y } if *y > 0 => {
        println!("click válido en x={x}, y={y}");
    }
    Evento::Click { .. } => println!("click fuera de pantalla"),
    Evento::Tecla(c @ ('a'..='z' | 'A'..='Z')) => println!("letra {c}"),
    _ => println!("otro evento"),
}

Leer este match es leer la estructura completa de una decisión: cada brazo es una proposición sobre la forma y el contenido del dato, y el compilador verifica que juntas cubren todo el espacio de casos. Esa densidad expresiva sin perder la verificación es lo que distingue al pattern matching de un switch.

💡
matches! para cuando solo quieres el sí o el no

Si únicamente necesitas saber si un valor encaja con un patrón, sin extraer nada, la macro matches!(valor, patrón) devuelve un bool y te ahorra el match de dos brazos con true y false. matches!(c, 'a'..='z') es más legible que su equivalente, y admite guardas: matches!(n, x if x > 0). Es el patrón usado como predicado, condensado en una expresión, ideal para un if o un filter.

El patrón es una pregunta y una extracción fundidas en un solo acto

Detente en lo que de verdad ocurre en la izquierda de una flecha. En la mayoría de los lenguajes, preguntar por la forma de un dato y extraer sus partes son dos pasos separados en el tiempo: primero un if o un instanceof que interroga, y después, ya dentro, un acceso o un casteo que extrae. Esa separación es una grieta: entre la pregunta y la extracción, nada garantiza que la respuesta siga siendo válida, y de ahí salen los casteos que fallan y los accesos a lo que no está. El patrón de Rust fusiona ambos actos en uno indivisible. Message::Move { x, y } es simultáneamente la pregunta “¿eres un Move?” y la extracción “dame tu x y tu y”, y el compilador garantiza que la segunda solo ocurre si la primera fue cierta. No hay ventana entre comprobar y usar. Por eso la exhaustividad y los binding modes y los rangos no son adornos sintácticos: son las piezas de un cálculo que te deja expresar, de una vez, la estructura completa de una decisión sobre datos, y verificarla entera antes de ejecutar una sola línea. Cuando interiorizas que un patrón es a la vez una proposición lógica y un destructor, dejas de escribir match como quien traduce un switch y empiezas a diseñar el flujo de control como quien escribe álgebra sobre la forma de los datos.

⚔️ Escribe patrones que otros escribirían con diez ifs
  1. Escribe un match sobre un i32 que use n @ 0..=9, un rango de negativos y una guarda if n % 2 == 0, todo en el mismo bloque.
  2. Define un enum Evento con tres variantes que compartan campos x e y y extrae la coordenada con un solo brazo usando |.
  3. Desestructura una struct Linea anidada en un único let irrefutable y explica por qué no necesita match.
  4. Usa { campo, .. } para leer un solo campo de un struct de seis, añade luego un séptimo campo y confirma que el patrón sigue compilando.
  5. Provoca a propósito el error de “variables no ligadas igual en un or-pattern” y lee el mensaje del compilador para entender la regla.