Práctica de Sintaxis de Patrones en Rust

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Pattern Syntax. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, discutiremos la sintaxis válida en los patrones y proporcionaremos ejemplos de cuándo y por qué podrías querer usar cada uno.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/mutable_variables -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/integer_types -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/boolean_type -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/string_type -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/function_syntax -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/expressions_statements -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/lifetime_specifiers -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} rust/method_syntax -.-> lab-100446{{"Práctica de Sintaxis de Patrones en Rust"}} end

Pattern Syntax

En esta sección, reunimos toda la sintaxis que es válida en los patrones y discutiremos por qué y cuándo podrías querer usar cada una.

Coincidencia con literales

Como viste en el Capítulo 6, puedes coincidir patrones contra literales directamente. El siguiente código da algunos ejemplos:

Nombre de archivo: src/main.rs

let x = 1;

match x {
    1 => println!("uno"),
    2 => println!("dos"),
    3 => println!("tres"),
    _ => println!("cualquier cosa"),
}

Este código imprime uno porque el valor en x es 1. Esta sintaxis es útil cuando quieres que tu código realice una acción si obtiene un valor concreto particular.

Coincidencia con variables con nombre

Las variables con nombre son patrones irrefutables que coinciden con cualquier valor, y las hemos utilizado muchas veces en este libro. Sin embargo, hay una complicación cuando se utilizan variables con nombre en expresiones match. Debido a que match inicia un nuevo ámbito, las variables declaradas como parte de un patrón dentro de la expresión match sombrearán a aquellas con el mismo nombre fuera de la construcción match, como es el caso de todas las variables. En la Lista 18-11, declaramos una variable llamada x con el valor Some(5) y una variable y con el valor 10. Luego creamos una expresión match en el valor x. Observa los patrones en los brazos de la coincidencia y println! al final, y trata de adivinar lo que imprimirá el código antes de ejecutarlo o leer más.

Nombre de archivo: src/main.rs

fn main() {
  1 let x = Some(5);
  2 let y = 10;

    match x {
      3 Some(50) => println!("Got 50"),
      4 Some(y) => println!("Matched, y = {y}"),
      5 _ => println!("Default case, x = {:?}", x),
    }

  6 println!("at the end: x = {:?}, y = {y}", x);
}

Lista 18-11: Una expresión match con un brazo que introduce una variable y sombreada

Analicemos lo que sucede cuando se ejecuta la expresión match. El patrón en el primer brazo de coincidencia [3] no coincide con el valor definido de x [1], por lo que el código continúa.

El patrón en el segundo brazo de coincidencia [4] introduce una nueva variable llamada y que coincidirá con cualquier valor dentro de un valor Some. Debido a que estamos en un nuevo ámbito dentro de la expresión match, esta es una nueva variable y, no la y que declaramos al principio con el valor 10 [2]. Esta nueva vinculación y coincidirá con cualquier valor dentro de un Some, que es lo que tenemos en x. Por lo tanto, esta nueva y se vincula al valor interno del Some en x. Ese valor es 5, por lo que la expresión de ese brazo se ejecuta e imprime Matched, y = 5.

Si x hubiera sido un valor None en lugar de Some(5), los patrones en los primeros dos brazos no habrían coincidido, por lo que el valor habría coincidido con el subrayado [5]. No introdujimos la variable x en el patrón del brazo del subrayado, por lo que la x en la expresión sigue siendo la x externa que no ha sido sombreada. En este caso hipotético, la coincidencia imprimiría Default case, x = None.

Cuando la expresión match finaliza, su ámbito finaliza, y también lo hace el ámbito de la y interna. El último println! [6] produce at the end: x = Some(5), y = 10.

Para crear una expresión match que compare los valores de la x externa y y, en lugar de introducir una variable sombreada, necesitaríamos usar una condición de guardia de coincidencia en su lugar. Hablaremos sobre las guardias de coincidencia en "Condicionales adicionales con guardias de coincidencia".

Varios patrones

En las expresiones match, puedes coincidir con varios patrones utilizando la sintaxis |, que es el operador o de patrones. Por ejemplo, en el siguiente código coincidimos el valor de x con los brazos de coincidencia, el primero de los cuales tiene una opción o, lo que significa que si el valor de x coincide con cualquiera de los valores en ese brazo, el código de ese brazo se ejecutará:

Nombre de archivo: src/main.rs

let x = 1;

match x {
    1 | 2 => println!("uno o dos"),
    3 => println!("tres"),
    _ => println!("cualquier cosa"),
}

Este código imprime uno o dos.

Coincidencia de rangos de valores con..=

La sintaxis ..= nos permite coincidir con un rango de valores inclusivo. En el siguiente código, cuando un patrón coincide con cualquiera de los valores dentro del rango dado, ese brazo se ejecutará:

Nombre de archivo: src/main.rs

let x = 5;

match x {
    1..=5 => println!("uno a cinco"),
    _ => println!("algo más"),
}

Si x es 1, 2, 3, 4 o 5, el primer brazo coincidirá. Esta sintaxis es más conveniente para múltiples valores de coincidencia que usar el operador | para expresar la misma idea; si tuviéramos que usar |, tendríamos que especificar 1 | 2 | 3 | 4 | 5. Especificar un rango es mucho más corto, especialmente si queremos coincidir, digamos, cualquier número entre 1 y 1.000!

El compilador comprueba que el rango no esté vacío en tiempo de compilación, y como los únicos tipos para los que Rust puede decir si un rango está vacío o no son char y valores numéricos, los rangos solo se permiten con valores numéricos o char.

Aquí hay un ejemplo que usa rangos de valores de char:

Nombre de archivo: src/main.rs

let x = 'c';

match x {
    'a'..='j' => println!("letra ASCII temprana"),
    'k'..='z' => println!("letra ASCII tardía"),
    _ => println!("algo más"),
}

Rust puede decir que 'c' está dentro del rango del primer patrón e imprime letra ASCII temprana.

Desestructuración para separar valores

También podemos usar patrones para desestructurar structs, enums y tuplas para usar diferentes partes de estos valores. Analicemos cada valor.

Desestructuración de structs

La Lista 18-12 muestra un struct Point con dos campos, x e y, que podemos descomponer usando un patrón con una declaración let.

Nombre de archivo: src/main.rs

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Lista 18-12: Desestructuración de los campos de un struct en variables separadas

Este código crea las variables a y b que coinciden con los valores de los campos x e y del struct p. Este ejemplo muestra que los nombres de las variables en el patrón no tienen que coincidir con los nombres de los campos del struct. Sin embargo, es común hacer coincidir los nombres de las variables con los nombres de los campos para que sea más fácil recordar qué variables provienen de qué campos. Debido a este uso común, y porque escribir let Point { x: x, y: y } = p; contiene mucha duplicación, Rust tiene una forma abreviada para los patrones que coinciden con los campos de struct: solo es necesario listar el nombre del campo del struct, y las variables creadas a partir del patrón tendrán los mismos nombres. La Lista 18-13 se comporta de la misma manera que el código de la Lista 18-12, pero las variables creadas en el patrón let son x e y en lugar de a y b.

Nombre de archivo: src/main.rs

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Lista 18-13: Desestructuración de campos de struct usando la forma abreviada de campo de struct

Este código crea las variables x e y que coinciden con los campos x e y de la variable p. El resultado es que las variables x e y contienen los valores del struct p.

También podemos desestructurar con valores literales como parte del patrón de struct en lugar de crear variables para todos los campos. Hacer esto nos permite probar algunos de los campos para valores particulares mientras creamos variables para desestructurar los otros campos.

En la Lista 18-14, tenemos una expresión match que separa los valores de Point en tres casos: puntos que se encuentran directamente sobre el eje x (lo que es cierto cuando y = 0), sobre el eje y (x = 0) o sobre ninguno de los ejes.

Nombre de archivo: src/main.rs

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("Sobre el eje x en {x}"),
        Point { x: 0, y } => println!("Sobre el eje y en {y}"),
        Point { x, y } => {
            println!("Sobre ninguno de los ejes: ({x}, {y})");
        }
    }
}

Lista 18-14: Desestructuración y coincidencia de valores literales en un patrón

El primer brazo coincidirá con cualquier punto que se encuentre sobre el eje x al especificar que el campo y coincide si su valor coincide con el literal 0. El patrón todavía crea una variable x que podemos usar en el código de este brazo.

Del mismo modo, el segundo brazo coincide con cualquier punto sobre el eje y al especificar que el campo x coincide si su valor es 0 y crea una variable y para el valor del campo y. El tercer brazo no especifica ningún literal, por lo que coincide con cualquier otro Point y crea variables para ambos campos x e y.

En este ejemplo, el valor p coincide con el segundo brazo por el hecho de que x contiene un 0, por lo que este código imprimirá Sobre el eje y en 7.

Recuerda que una expresión match deja de comprobar los brazos una vez que ha encontrado el primer patrón que coincide, por lo que aunque Point { x: 0, y: 0} está sobre el eje x y el eje y, este código solo imprimirá Sobre el eje x en 0.

Desestructuración de enums

Hemos desestructurado enums en este libro (por ejemplo, Lista 6-5), pero aún no hemos discutido explícitamente que el patrón para desestructurar un enum corresponde a la forma en que se define los datos almacenados dentro del enum. Como ejemplo, en la Lista 18-15 usamos el enum Message de la Lista 6-2 y escribimos una expresión match con patrones que desestructurarán cada valor interno.

Nombre de archivo: src/main.rs

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
  1 let msg = Message::ChangeColor(0, 160, 255);

    match msg {
      2 Message::Quit => {
            println!(
                "La variante Quit no tiene datos para desestructurar."
            );
        }
      3 Message::Move { x, y } => {
            println!(
                "Mover en la dirección x {x}, en la dirección y {y}"
            );
        }
      4 Message::Write(text) => {
            println!("Mensaje de texto: {text}");
        }
      5 Message::ChangeColor(r, g, b) => println!(
            "Cambiar color a rojo {r}, verde {g}, y azul {b}"
        ),
    }
}

Lista 18-15: Desestructuración de variantes de enum que contienen diferentes tipos de valores

Este código imprimirá Cambiar color a rojo 0, verde 160, y azul 255. Intenta cambiar el valor de msg [1] para ver el código de los otros brazos en ejecución.

Para las variantes de enum sin ningún dato, como Message::Quit [2], no podemos desestructurar el valor más allá. Solo podemos coincidir con el valor literal Message::Quit, y no hay variables en ese patrón.

Para las variantes de enum similares a structs, como Message::Move [3], podemos usar un patrón similar al patrón que especificamos para coincidir con structs. Después del nombre de la variante, ponemos llaves y luego listamos los campos con variables para que sepamos descomponer los fragmentos para usarlos en el código de este brazo. Aquí usamos la forma abreviada como lo hicimos en la Lista 18-13.

Para las variantes de enum similares a tuplas, como Message::Write que contiene una tupla con un elemento [4] y Message::ChangeColor que contiene una tupla con tres elementos [5], el patrón es similar al patrón que especificamos para coincidir con tuplas. El número de variables en el patrón debe coincidir con el número de elementos en la variante con la que estamos coincidiendo.

Desestructuración de structs y enums anidados

Hasta ahora, todos nuestros ejemplos han sido para coincidir con structs o enums a un nivel de profundidad, pero la coincidencia también puede funcionar en elementos anidados. Por ejemplo, podemos refactorizar el código de la Lista 18-15 para admitir colores RGB y HSV en el mensaje ChangeColor, como se muestra en la Lista 18-16.

Nombre de archivo: src/main.rs

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
            "Cambiar color a rojo {r}, verde {g}, y azul {b}"
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Cambiar color a matiz {h}, saturación {s}, valor {v}"
        ),
        _ => (),
    }
}

Lista 18-16: Coincidencia en enums anidados

El patrón del primer brazo en la expresión match coincide con una variante de enum Message::ChangeColor que contiene una variante Color::Rgb; luego el patrón se enlaza a los tres valores i32 internos. El patrón del segundo brazo también coincide con una variante de enum Message::ChangeColor, pero el enum interno coincide con Color::Hsv en lugar. Podemos especificar estas condiciones complejas en una sola expresión match, aunque se involucren dos enums.

Desestructuración de structs y tuplas

Podemos mezclar, combinar y anidar patrones de desestructuración de maneras aún más complejas. El siguiente ejemplo muestra una desestructuración complicada donde anidamos structs y tuplas dentro de una tupla y desestructuramos todos los valores primitivos:

let ((feet, inches), Point { x, y }) =
    ((3, 10), Point { x: 3, y: -10 });

Este código nos permite descomponer tipos complejos en sus partes componentes para que podamos usar por separado los valores en los que estamos interesados.

La desestructuración con patrones es una forma conveniente de usar fragmentos de valores, como el valor de cada campo en un struct, de forma independiente los unos de los otros.

Ignorar valores en un patrón

Has visto que a veces es útil ignorar valores en un patrón, como en el último brazo de una match, para obtener un caso general que en realidad no hace nada pero abarca todos los valores posibles restantes. Hay varias maneras de ignorar valores enteros o partes de valores en un patrón: usando el patrón _ (que ya has visto), usando el patrón _ dentro de otro patrón, usando un nombre que empieza con un guion bajo o usando .. para ignorar las partes restantes de un valor. Vamos a explorar cómo y por qué usar cada uno de estos patrones.

Un valor entero con _

Hemos usado el guion bajo como un patrón comodín que coincidirá con cualquier valor pero no se enlazará al valor. Esto es especialmente útil como el último brazo en una expresión match, pero también podemos usarlo en cualquier patrón, incluyendo parámetros de función, como se muestra en la Lista 18-17.

Nombre de archivo: src/main.rs

fn foo(_: i32, y: i32) {
    println!("Este código solo usa el parámetro y: {y}");
}

fn main() {
    foo(3, 4);
}

Lista 18-17: Usando _ en una firma de función

Este código ignorará completamente el valor 3 pasado como primer argumento y imprimirá Este código solo usa el parámetro y: 4.

En la mayoría de los casos, cuando ya no necesitas un parámetro de función en particular, cambiarías la firma para que no incluya el parámetro no utilizado. Ignorar un parámetro de función puede ser especialmente útil en casos donde, por ejemplo, estás implementando un trato y necesitas una cierta firma de tipo, pero el cuerpo de la función en tu implementación no necesita uno de los parámetros. Entonces evitas recibir una advertencia del compilador sobre parámetros de función no utilizados, como ocurriría si usaras un nombre en lugar de eso.

Partes de un valor con un _ anidado

También podemos usar _ dentro de otro patrón para ignorar solo parte de un valor. Por ejemplo, cuando queremos probar solo parte de un valor pero no tenemos uso de las otras partes en el código correspondiente que queremos ejecutar. La Lista 18-18 muestra el código responsable de administrar el valor de una configuración. Los requisitos comerciales son que no se debe permitir que el usuario sobrescriba una personalización existente de una configuración, pero puede anular la configuración y asignarle un valor si actualmente está sin valor.

Nombre de archivo: src/main.rs

let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("No se puede sobrescribir un valor personalizado existente");
    }
    _ => {
        setting_value = new_setting_value;
    }
}

println!("configuración es {:?}", setting_value);

Lista 18-18: Usando un guion bajo dentro de patrones que coinciden con variantes Some cuando no necesitamos usar el valor dentro de Some

Este código imprimirá No se puede sobrescribir un valor personalizado existente y luego configuración es Some(5). En el primer brazo de coincidencia, no necesitamos coincidir con ni usar los valores dentro de ninguna variante Some, pero sí necesitamos probar el caso en el que setting_value y new_setting_value son la variante Some. En ese caso, imprimimos la razón por la que no se cambia setting_value, y no se cambia.

En todos los demás casos (si setting_value o new_setting_value es None) expresados por el patrón _ en el segundo brazo, queremos permitir que new_setting_value se convierta en setting_value.

También podemos usar guiones bajos en múltiples lugares dentro de un patrón para ignorar valores particulares. La Lista 18-19 muestra un ejemplo de ignorar el segundo y cuarto valores en una tupla de cinco elementos.

Nombre de archivo: src/main.rs

let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, _, third, _, fifth) => {
        println!("Algunos números: {first}, {third}, {fifth}");
    }
}

Lista 18-19: Ignorar múltiples partes de una tupla

Este código imprimirá Algunos números: 2, 8, 32, y los valores 4 y 16 serán ignorados.

Una variable no utilizada comenzando su nombre con _

Si creas una variable pero no la usas en ningún lugar, Rust generalmente emitirá una advertencia porque una variable no utilizada podría ser un error. Sin embargo, a veces es útil poder crear una variable que aún no utilizarás, como cuando estás haciendo un prototipo o simplemente comenzando un proyecto. En esta situación, puedes decirle a Rust que no te advierta sobre la variable no utilizada comenzando el nombre de la variable con un guion bajo. En la Lista 18-20, creamos dos variables no utilizadas, pero cuando compilamos este código, solo deberíamos recibir una advertencia sobre una de ellas.

Nombre de archivo: src/main.rs

fn main() {
    let _x = 5;
    let y = 10;
}

Lista 18-20: Comenzar el nombre de una variable con un guion bajo para evitar advertencias de variables no utilizadas

Aquí, recibimos una advertencia sobre no utilizar la variable y, pero no recibimos una advertencia sobre no utilizar _x.

Tenga en cuenta que hay una diferencia sutil entre usar solo _ y usar un nombre que comienza con un guion bajo. La sintaxis _x todavía enlaza el valor a la variable, mientras que _ no enlaza en absoluto. Para mostrar un caso en el que esta distinción importa, la Lista 18-21 nos proporcionará un error.

Nombre de archivo: src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
    println!("encontró una cadena");
}

println!("{:?}", s);

Lista 18-21: Una variable no utilizada que comienza con un guion bajo todavía enlaza el valor, lo que podría tomar posesión del valor.

Recibiremos un error porque el valor s todavía se moverá a _s, lo que nos impide usar s nuevamente. Sin embargo, usar el guion bajo por sí solo nunca se enlaza al valor. La Lista 18-22 se compilará sin errores porque s no se mueve a _.

Nombre de archivo: src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("encontró una cadena");
}

println!("{:?}", s);

Lista 18-22: Usar un guion bajo no enlaza el valor.

Este código funciona perfectamente porque nunca enlazamos s a nada; no se mueve.

Partes restantes de un valor con..

Con valores que tienen muchas partes, podemos usar la sintaxis .. para usar partes específicas e ignorar el resto, evitando la necesidad de listar guiones bajos para cada valor ignorado. El patrón .. ignora cualquier parte de un valor que no hayamos coincidido explícitamente en el resto del patrón. En la Lista 18-23, tenemos una struct Point que almacena una coordenada en un espacio tridimensional. En la expresión match, queremos operar solo en la coordenada x e ignorar los valores en los campos y y z.

Nombre de archivo: src/main.rs

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x,.. } => println!("x es {x}"),
}

Lista 18-23: Ignorar todos los campos de un Point excepto x usando ..

Listamos el valor de x y luego simplemente incluimos el patrón ... Esto es más rápido que tener que listar y: _ y z: _, especialmente cuando estamos trabajando con structs que tienen muchos campos en situaciones donde solo uno o dos campos son relevantes.

La sintaxis .. se expandirá a tantos valores como sea necesario. La Lista 18-24 muestra cómo usar .. con una tupla.

Nombre de archivo: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first,.., last) => {
            println!("Algunos números: {first}, {last}");
        }
    }
}

Lista 18-24: Coincidir solo con los primeros y últimos valores en una tupla e ignorar todos los demás valores

En este código, los primeros y últimos valores se coinciden con first y last. El .. coincidirá e ignorará todo lo que está en el medio.

Sin embargo, usar .. debe ser inequívoco. Si no es claro qué valores se pretenden coincidir y cuáles deben ser ignorados, Rust nos dará un error. La Lista 18-25 muestra un ejemplo de usar .. de manera ambigua, por lo que no se compilará.

Nombre de archivo: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second,..) => {
            println!("Algunos números: {second}");
        },
    }
}

Lista 18-25: Un intento de usar .. de manera ambigua

Cuando compilamos este ejemplo, obtenemos este error:

error: `..` solo se puede usar una vez por patrón de tupla
 --> src/main.rs:5:22
  |
5 |         (.., second,..) => {
  |          --          ^^ solo se puede usar una vez por patrón de tupla
  |          |
  |          previamente usado aquí

Es imposible para Rust determinar cuántos valores de la tupla se deben ignorar antes de coincidir un valor con second y luego cuántos valores adicionales se deben ignorar después. Este código podría significar que queremos ignorar 2, enlazar second a 4 y luego ignorar 8, 16 y 32; o que queremos ignorar 2 y 4, enlazar second a 8 y luego ignorar 16 y 32; y así sucesivamente. El nombre de variable second no significa nada especial para Rust, por lo que obtenemos un error del compilador porque usar .. en dos lugares como este es ambiguo.

Condicionales adicionales con filtros de coincidencia

Un filtro de coincidencia es una condición if adicional, especificada después del patrón en un brazo de match, que también debe coincidir para que se elija ese brazo. Los filtros de coincidencia son útiles para expresar ideas más complejas que lo que permite un patrón solo.

La condición puede usar variables creadas en el patrón. La Lista 18-26 muestra un match donde el primer brazo tiene el patrón Some(x) y también tiene un filtro de coincidencia de if x % 2 == 0 (que será true si el número es par).

Nombre de archivo: src/main.rs

let num = Some(4);

match num {
    Some(x) if x % 2 == 0 => println!("El número {x} es par"),
    Some(x) => println!("El número {x} es impar"),
    None => (),
}

Lista 18-26: Agregar un filtro de coincidencia a un patrón

Este ejemplo imprimirá El número 4 es par. Cuando num se compara con el patrón en el primer brazo, coincide porque Some(4) coincide con Some(x). Luego, el filtro de coincidencia verifica si el residuo de dividir x entre 2 es igual a 0, y como es así, se selecciona el primer brazo.

Si num hubiera sido Some(5) en lugar de eso, el filtro de coincidencia en el primer brazo habría sido false porque el residuo de 5 dividido entre 2 es 1, que no es igual a 0. Rust entonces pasaría al segundo brazo, que coincidiría porque el segundo brazo no tiene un filtro de coincidencia y, por lo tanto, coincide con cualquier variante Some.

No hay forma de expresar la condición if x % 2 == 0 dentro de un patrón, por lo que el filtro de coincidencia nos da la capacidad de expresar esta lógica. La desventaja de esta expresividad adicional es que el compilador no intenta comprobar la exhaustividad cuando se involucran expresiones de filtros de coincidencia.

En la Lista 18-11, mencionamos que podríamos usar filtros de coincidencia para resolver nuestro problema de sombreado de patrones. Recuerde que creamos una nueva variable dentro del patrón en la expresión match en lugar de usar la variable fuera del match. Esa nueva variable significaba que no podíamos probar contra el valor de la variable externa. La Lista 18-27 muestra cómo podemos usar un filtro de coincidencia para solucionar este problema.

Nombre de archivo: src/main.rs

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Obtuvo 50"),
        Some(n) if n == y => println!("Coincidió, n = {n}"),
        _ => println!("Caso predeterminado, x = {:?}", x),
    }

    println!("al final: x = {:?}, y = {y}", x);
}

Lista 18-27: Usar un filtro de coincidencia para probar la igualdad con una variable externa

Este código ahora imprimirá Caso predeterminado, x = Some(5). El patrón en el segundo brazo de coincidencia no introduce una nueva variable y que sombreada la y externa, lo que significa que podemos usar la y externa en el filtro de coincidencia. En lugar de especificar el patrón como Some(y), que habría sombreado la y externa, especificamos Some(n). Esto crea una nueva variable n que no sombreada nada porque no hay una variable n fuera del match.

El filtro de coincidencia if n == y no es un patrón y, por lo tanto, no introduce nuevas variables. Esta y es la y externa en lugar de una nueva y sombreada, y podemos buscar un valor que tenga el mismo valor que la y externa comparando n con y.

También puede usar el operador o | en un filtro de coincidencia para especificar múltiples patrones; la condición del filtro de coincidencia se aplicará a todos los patrones. La Lista 18-28 muestra la precedencia al combinar un patrón que usa | con un filtro de coincidencia. La parte importante de este ejemplo es que el filtro de coincidencia if y se aplica a 4, 5, y 6, aunque puede parecer que if y solo se aplica a 6.

Nombre de archivo: src/main.rs

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("sí"),
    _ => println!("no"),
}

Lista 18-28: Combinar múltiples patrones con un filtro de coincidencia

La condición de coincidencia establece que el brazo solo coincide si el valor de x es igual a 4, 5 o 6 y si y es true. Cuando se ejecuta este código, el patrón del primer brazo coincide porque x es 4, pero el filtro de coincidencia if y es false, por lo que el primer brazo no se elige. El código pasa al segundo brazo, que coincide, y este programa imprime no. La razón es que la condición if se aplica al patrón completo 4 | 5 | 6, no solo al último valor 6. En otras palabras, la precedencia de un filtro de coincidencia en relación con un patrón se comporta así:

(4 | 5 | 6) if y =>...

en lugar de esto:

4 | 5 | (6 if y) =>...

Después de ejecutar el código, el comportamiento de precedencia es evidente: si el filtro de coincidencia se aplicara solo al último valor en la lista de valores especificados usando el operador |, el brazo habría coincidido y el programa habría impreso .

@ Vinculaciones

El operador arroba @ nos permite crear una variable que almacena un valor al mismo tiempo que estamos probando ese valor para una coincidencia de patrón. En la Lista 18-29, queremos probar que el campo id de un Message::Hello esté en el rango 3..=7. También queremos vincular el valor a la variable id_variable para poder usarlo en el código asociado al brazo. Podríamos nombrar esta variable id, igual que el campo, pero para este ejemplo usaremos un nombre diferente.

Nombre de archivo: src/main.rs

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello {
        id: id_variable @ 3..=7,
    } => println!("Encontré un id en el rango: {id_variable}"),
    Message::Hello { id: 10..=12 } => {
        println!("Encontré un id en otro rango")
    }
    Message::Hello { id } => println!("Algún otro id: {id}"),
}

Lista 18-29: Usando @ para vincularse a un valor en un patrón mientras también lo prueba

Este ejemplo imprimirá Encontré un id en el rango: 5. Al especificar id_variable @ antes del rango 3..=7, estamos capturando cualquier valor que coincidiera con el rango mientras también probamos que el valor coincidiera con el patrón de rango.

En el segundo brazo, donde solo tenemos un rango especificado en el patrón, el código asociado al brazo no tiene una variable que contenga el valor real del campo id. El valor del campo id podría haber sido 10, 11 o 12, pero el código que va con ese patrón no sabe cuál es. El código del patrón no puede usar el valor del campo id porque no hemos guardado el valor de id en una variable.

En el último brazo, donde hemos especificado una variable sin un rango, sí tenemos el valor disponible para usarlo en el código del brazo en una variable llamada id. La razón es que hemos usado la sintaxis abreviada de campo de struct. Pero no hemos aplicado ninguna prueba al valor en el campo id en este brazo, como hicimos con los dos primeros brazos: cualquier valor coincidiría con este patrón.

Usar @ nos permite probar un valor y guardarlo en una variable dentro de un solo patrón.

Resumen

¡Felicitaciones! Has completado el laboratorio de Sintaxis de Patrones. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.