Coincidencia de patrones en Rust
En la página de Match aprendiste lo básico: ramas, _, agrupar valores con | y rangos. Eso es solo el principio. Un patrón en Rust también puede mirar dentro de un valor, sacar las partes que necesitas y comprobar condiciones extra, todo en una sola línea.
Piensa en un patrón como un molde para galletas. Lo presionas sobre un valor. Si la forma encaja, Rust te entrega las piezas de dentro. Si no encaja, Rust prueba la siguiente rama. Cuando lo entiendas, usarás patrones en todas partes: en match, en let, en los parámetros de las funciones y en if let.
Guardas: añadir un if
Sección titulada «Guardas: añadir un if»A veces la forma no basta y necesitas una comprobación extra. Una guarda (match guard) es una condición if escrita después del patrón. La rama solo se ejecuta si el patrón encaja y la condición es verdadera.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "un frío glacial", 0 => "justo cero", t if t < 25 => "agradable", _ => "calor", }; println!("{t}°C: {feeling}"); }}Salida:
-5°C: un frío glacial0°C: justo cero18°C: agradable35°C: calorRust revisa las ramas de arriba hacia abajo, así que el orden importa. 18 no pasa la primera guarda, no es 0 y sí cumple t < 25, así que es “agradable”.
Desestructurar tuplas y structs
Sección titulada «Desestructurar tuplas y structs»Desestructurar (destructuring) significa separar un valor en sus partes. Para una tupla, escribe el patrón con la misma forma que la tupla. Para un struct, nombra los campos que quieres.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("el origen"), (x, 0) => println!("en el eje x, en {x}"), (0, y) => println!("en el eje y, en {y}"), (x, y) => println!("en otro sitio: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("en el eje y, y = {y}"), Point { x, y: 0 } => println!("en el eje x, x = {x}"), Point { x, y } => println!("en ({x}, {y})"), }}Salida:
en otro sitio: (3, -7)en el eje y, y = 5Fíjate en la mezcla. 0 es un valor fijo que tiene que coincidir exactamente. x e y son variables nuevas que atrapan lo que haya en esa posición. Point { x, y } es la forma corta de Point { x: x, y: y }.
Desestructurar enums con datos
Sección titulada «Desestructurar enums con datos»Aquí es donde los patrones brillan de verdad. Una variante de un enum puede llevar datos, y un patrón puede desempaquetarlos mientras comprueba qué variante tienes.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("área = {:.2}", area(shape)); }}Salida:
área = 12.57área = 13.50área = 2.25No puedes llegar a radius sin demostrar antes que el valor es un Circle. Esa es la ventaja en seguridad: el compilador no te deja leer datos de la variante equivocada.
Patrones anidados y enlaces con @
Sección titulada «Patrones anidados y enlaces con @»Los patrones pueden ir dentro de otros patrones, con tanta profundidad como quieras. Aquí, un enum Command contiene un enum Direction, y una rama encaja con los dos niveles a la vez.
Un enlace con @ (@ binding) te permite comprobar un valor contra un patrón y guardarlo en una variable. n @ 1..=9 significa “comprueba que esté entre 1 y 9, y llámalo n”.
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("arriba {steps} (paso pequeño)"), Command::Move(Direction::Up, steps) => println!("arriba {steps} (¡gran salto!)"), Command::Move(Direction::Down, steps) => println!("abajo {steps}"), Command::Say(text) => println!("diciendo: {text}"), Command::Quit => println!("adiós"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("hola"))); run(Command::Quit);}Salida:
arriba 3 (paso pequeño)arriba 50 (¡gran salto!)abajo 2diciendo: holaadiósSin @, un rango como 1..=9 comprueba el valor pero lo descarta. Con @, puedes quedártelo.
Ignorar partes con _ y ..
Sección titulada «Ignorar partes con _ y ..»Muchas veces solo te interesan algunas de las piezas. Usa _ para saltarte un valor. Usa .. para saltarte “todo lo demás”.
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("rojo = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("primero = {first}, último = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} está en el nivel {level}");}Salida:
rojo = 255primero = 1, último = 5Mira está en el nivel 7.. solo puede aparecer una vez en un patrón. (first, .., last) está bien, pero (.., middle, ..) no: Rust no podría saber a qué valor te refieres.
Patrones en let y en los parámetros de funciones
Sección titulada «Patrones en let y en los parámetros de funciones»Cada let es, en secreto, un patrón. Y cada parámetro de una función también. Eso significa que puedes desestructurar justo donde llega un valor.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Salida:
1920x1080x: 4, y: -2[3, 7, 11]La closure |&(a, b)| a + b usa la misma idea. Cada elemento es un &(i32, i32), y el patrón lo desempaqueta directamente en a y b. Verás esto muy a menudo con los iteradores.
matches! y let-else
Sección titulada «matches! y let-else»Dos herramientas prácticas te ahorran escribir un match completo.
matches!(valor, patrón) devuelve true o false. Es perfecta dentro de un if o de .filter() cuando solo necesitas un sí o un no. Las guardas también funcionan aquí.
let-else resuelve el caso de “desempaqueta esto o sal ya”. Se escribe let PATRÓN = valor else { ... };. Si el patrón encaja, las variables quedan disponibles para el resto de la función. Si no, se ejecuta el bloque else, y este tiene que salir: con return, break, continue o un panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' no es un número"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} aprobados");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("treinta"));}Salida:
true3 aprobadosSome(31)'treinta' no es un númeroNonelet-else mantiene plano el “camino feliz”. En lugar de meter tu código dentro de un if let ... { }, te ocupas primero del caso malo y sigues adelante. Combínalo con valores Option y Result.
Errores comunes
Sección titulada «Errores comunes»Las guardas no cuentan para “todos los casos cubiertos”. El compilador no puede razonar sobre tus condiciones if, así que esto falla aunque las dos guardas cubran todos los números:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("negativo"), x if x >= 0 => println!("cero o positivo"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityEl compilador te dice que el patrón no es exhaustivo: no está cubierto el rango completo de i32, y añade que las ramas con guarda no cuentan para esa comprobación. La solución: haz que la última rama sea un comodín simple, como _ => println!("cero o positivo").
Un nombre de variable en un patrón es una variable nueva, no una comparación. Esto parece comprobar guess == expected, pero no lo hace:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("¡Correcto!"), _ => println!("Incorrecto"), }}Compila e imprime ¡Correcto!, porque expected en la rama solo crea una variable nueva que atrapa cualquier valor. Rust te avisa de que el patrón _ es inalcanzable (unreachable pattern), porque ningún valor puede llegar hasta él:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("¡Correcto!"), | -------- matches any value6 | _ => println!("Incorrecto"), | ^ no value can reach thisLa solución es una guarda: g if g == expected => println!("¡Correcto!").
Práctica
Sección titulada «Práctica»1. FizzBuzz con una tupla
Sección titulada «1. FizzBuzz con una tupla»Imprime los números del 9 al 15, pero escribe Fizz para los múltiplos de 3, Buzz para los múltiplos de 5 y FizzBuzz para los múltiplos de ambos. Hazlo con un solo match sobre una tupla, en lugar de una cadena de if. Pista: haz match sobre (n % 3, n % 5). Un resto de 0 significa “es divisible”, y _ significa “me da igual”.
Ver solución
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Salida:
FizzBuzz11Fizz1314FizzBuzz(0, 0) tiene que ir primero. Si (0, _) estuviera antes, el 15 imprimiría Fizz.
2. Movimientos bancarios
Sección titulada «2. Movimientos bancarios»Define enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Empieza con un balance (saldo) de 100 y aplica esta lista en orden: depósito de 50, retiro de 30, retiro de 500 y una comisión. La comisión cuesta 5. Un retiro mayor que el saldo debe rechazarse con un mensaje. Al final, imprime el saldo. Pista: usa una guarda, Transaction::Withdraw(amount) if amount <= balance, seguida de una segunda rama Withdraw para el caso rechazado.
Ver solución
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("rechazado: no se pueden retirar {amount}"), Transaction::Fee => balance -= 5, } }
println!("saldo = {balance}");}Salida:
rechazado: no se pueden retirar 500saldo = 115La segunda rama Withdraw no tiene guarda, así que atrapa todos los retiros que la primera rama rechazó. Sin ella, el match no compilaría: las guardas no cuentan cuando Rust comprueba que todos los casos están cubiertos.
Resumen
Sección titulada «Resumen»- Una guarda (
patrón if condición) añade una comprobación extra a una rama. - Los patrones pueden desestructurar tuplas, structs y variantes de enums, y se pueden anidar.
nombre @ patróncomprueba un valor y, a la vez, lo guarda en una variable._se salta un valor;..se salta el resto. Los patrones también funcionan enlet, en los parámetros de funciones y en las closures.matches!da untrue/falserápido; let-else desempaqueta un valor o sale antes de tiempo.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué hace n @ 1..=9 en una rama de match?
2. En let-else, ¿qué tiene que hacer el bloque else?
3. ¿Qué obtienes con let (first, .., last) = (1, 2, 3, 4, 5);?
4. ¿Por qué un match que solo tiene ramas con guardas suele no compilar?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Patrones y coincidencias — El libro de Rust (en inglés): el capítulo oficial sobre todos los lugares donde pueden aparecer patrones y cuándo pueden no encajar.
- Sintaxis de patrones — El libro de Rust (en inglés): un recorrido por todas las formas de patrón, desde rangos y desestructuración hasta guardas y enlaces con
@. - Seguir por el camino feliz con let…else — El libro de Rust (en inglés): un ejemplo sencillo y bien explicado de let-else.
- Desestructuración — Rust by Example (en inglés): ejemplos ejecutables de cómo desestructurar tuplas, arrays, enums, punteros y structs.
- La macro
matches!— documentación de std (en inglés): la sintaxis exacta de la macro, incluida la forma de añadir una guarda.