La Estructura de Control de Flujo match

Beginner

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

Introducción

Bienvenido a La Estructura de Control de Flujo match. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, exploraremos la poderosa estructura de control de flujo match en Rust, que permite la coincidencia de patrones y la ejecución de código basado en el patrón coincidido.

La Estructura de Control de Flujo match

Rust tiene una estructura de control de flujo extremadamente poderosa llamada match que te permite comparar un valor contra una serie de patrones y luego ejecutar código según el patrón que coincida. Los patrones pueden estar compuestos por valores literales, nombres de variables, comodines y muchas otras cosas; el Capítulo 18 aborda todos los diferentes tipos de patrones y lo que hacen. El poder de match proviene de la expresividad de los patrones y del hecho de que el compilador confirma que todos los casos posibles se manejan.

Piensa en una expresión match como en una máquina para clasificar monedas: las monedas deslizan por una pista con orificios de diferentes tamaños a lo largo de ella, y cada moneda cae a través del primer orificio que encuentra y en el que encaja. De la misma manera, los valores pasan por cada patrón en un match, y en el primer patrón en el que el valor "encaja", el valor cae en el bloque de código asociado para ser utilizado durante la ejecución.

Hablando de monedas, ¡usemoslas como ejemplo con match! Podemos escribir una función que tome una moneda estadounidense desconocida y, de manera similar a la máquina contadora, determine de qué moneda se trata y devuelva su valor en centavos, como se muestra en la Lista 6-3.

1 enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  2 match coin {
      3 Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Lista 6-3: Un enum y una expresión match que tiene las variantes del enum como sus patrones

Analicemos el match en la función value_in_cents. Primero, listamos la palabra clave match seguida de una expresión, que en este caso es el valor coin [2]. Esto parece muy similar a una expresión utilizada con if, pero hay una gran diferencia: con if, la expresión debe devolver un valor booleano, pero aquí puede devolver cualquier tipo. El tipo de coin en este ejemplo es el enum Coin que definimos en [1].

A continuación están los brazos del match. Un brazo tiene dos partes: un patrón y un poco de código. El primer brazo aquí tiene un patrón que es el valor Coin::Penny y luego el operador => que separa el patrón y el código a ejecutar [3]. El código en este caso es simplemente el valor 1. Cada brazo está separado del siguiente con una coma.

Cuando se ejecuta la expresión match, compara el valor resultante con el patrón de cada brazo, en orden. Si un patrón coincide con el valor, se ejecuta el código asociado a ese patrón. Si ese patrón no coincide con el valor, la ejecución continúa con el siguiente brazo, al igual que en una máquina para clasificar monedas. Podemos tener tantos brazos como necesitemos: en la Lista 6-3, nuestro match tiene cuatro brazos.

El código asociado a cada brazo es una expresión, y el valor resultante de la expresión en el brazo coincidente es el valor que se devuelve para la expresión match completa.

Normalmente no usamos llaves si el código del brazo del match es corto, como es el caso en la Lista 6-3 donde cada brazo solo devuelve un valor. Si quieres ejecutar múltiples líneas de código en un brazo del match, debes usar llaves, y entonces la coma que sigue al brazo es opcional. Por ejemplo, el siguiente código imprime "¡Moneda de un centavo, suerte!" cada vez que se llama al método con un Coin::Penny, pero todavía devuelve el último valor del bloque, 1:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Patrones que se asocian a Valores

Otra característica útil de los brazos del match es que pueden enlazarse a las partes de los valores que coinciden con el patrón. Así es como podemos extraer valores de las variantes del enum.

Como ejemplo, cambiemos una de las variantes de nuestro enum para que contenga datos dentro de ella. De 1999 a 2008, la moneeda estadounidense de 25 centavos tenía diferentes diseños para cada una de las 50 estados en un lado. Ninguna otra moneda tenía diseños de estados, por lo que solo las monedas de 25 centavos tienen este valor extra. Podemos agregar esta información a nuestro enum cambiando la variante Quarter para incluir un valor de UsState almacenado dentro de ella, como se ha hecho en la Lista 6-4.

#[derive(Debug)] // para que podamos inspeccionar el estado en un momento
enum UsState {
    Alabama,
    Alaska,
    --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

Lista 6-4: Un enum Coin en el que la variante Quarter también contiene un valor de UsState

Imaginemos que un amigo está intentando recolectar las 50 monedas de 25 centavos de los estados. Mientras clasificamos nuestro cambio suelto por tipo de moneda, también llamaremos el nombre del estado asociado a cada moneda de 25 centavos para que si es una que nuestro amigo no tiene, puedan agregarla a su colección.

En la expresión match de este código, agregamos una variable llamada state al patrón que coincide con los valores de la variante Coin::Quarter. Cuando un Coin::Quarter coincide, la variable state se enlazará al valor del estado de esa moneda de 25 centavos. Luego podemos usar state en el código de ese brazo, como sigue:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

Si llamáramos a value_in_cents(Coin::Quarter(UsState::Alaska)), coin sería Coin::Quarter(UsState::Alaska). Cuando comparamos ese valor con cada uno de los brazos del match, ninguno de ellos coincide hasta que llegamos a Coin::Quarter(state). En ese momento, el enlace para state será el valor UsState::Alaska. Luego podemos usar ese enlace en la expresión println!, obteniendo así el valor interno de estado de la variante Coin para Quarter.

Coincidencia con Option<T>

En la sección anterior, queríamos extraer el valor interno T del caso Some al usar Option<T>; también podemos manejar Option<T> usando match, como lo hicimos con el enum Coin! En lugar de comparar monedas, compararemos las variantes de Option<T>, pero la forma en que funciona la expresión match sigue siendo la misma.

Digamos que queremos escribir una función que tome una Option<i32> y, si hay un valor dentro, sume 1 a ese valor. Si no hay un valor dentro, la función debe devolver el valor None y no intentar realizar ninguna operación.

Esta función es muy fácil de escribir, gracias a match, y se verá como en la Lista 6-5.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
      1 None => None,
      2 Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4

Lista 6-5: Una función que utiliza una expresión match en una Option<i32>

Analicemos con más detalle la primera ejecución de plus_one. Cuando llamamos a plus_one(five) [3], la variable x en el cuerpo de plus_one tendrá el valor Some(5). Luego la comparamos con cada brazo del match:

None => None,

El valor Some(5) no coincide con el patrón None [1], así que continuamos con el siguiente brazo:

Some(i) => Some(i + 1),

¿Coincide Some(5) con Some(i) [2]? ¡Por supuesto que sí! Tenemos la misma variante. La i se enlaza al valor contenido en Some, por lo que i toma el valor 5. Luego se ejecuta el código del brazo del match, por lo que sumamos 1 al valor de i y creamos un nuevo valor Some con nuestro total 6 dentro.

Ahora consideremos la segunda llamada a plus_one en la Lista 6-5, donde x es None [4]. Entramos en el match y comparamos con el primer brazo [1].

¡Coincide! No hay valor al que sumar, así que el programa se detiene y devuelve el valor None del lado derecho de =>. Debido a que el primer brazo coincidió, no se comparan otros brazos.

Combinar match y enum es útil en muchas situaciones. Verás este patrón con frecuencia en el código de Rust: hacer coincidir contra un enum, enlazar una variable a los datos internos y luego ejecutar código en función de ello. Al principio es un poco complicado, pero una vez que te acostumbras a él, desearás tenerlo en todos los lenguajes. Es consistentemente una de las favoritas de los usuarios.

Las Coincidencias Son Exhaustivas

Hay otro aspecto de match que debemos discutir: los patrones de los brazos deben cubrir todas las posibilidades. Considere esta versión de nuestra función plus_one, que tiene un error y no se compilará:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

No manejamos el caso None, por lo que este código causará un error. Por suerte, es un error que Rust sabe cómo detectar. Si intentamos compilar este código, obtendremos este error:

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
  note: `Option<i32>` defined here
      = note: el valor coincidido es del tipo `Option<i32>`
help: asegúrese de que todos los casos posibles estén siendo manejados agregando
un brazo de coincidencia con un patrón comodín o un patrón explícito como
se muestra
    |
4   ~             Some(i) => Some(i + 1),
5   ~             None => todo!(),
    |

Rust sabe que no cubrimos cada caso posible e incluso sabe qué patrón olvidamos. Las coincidencias en Rust son exhaustivas: debemos agotar cada última posibilidad para que el código sea válido. Especialmente en el caso de Option<T>, cuando Rust nos impide olvidar manejar explícitamente el caso None, nos protege de asumir que tenemos un valor cuando podríamos tener null, evitando así el error de mil millones discutido anteriormente.

Patrones Genéricos y el Comodín _

Usando enum, también podemos tomar acciones especiales para algunos valores particulares, pero para todos los demás valores tomar una acción predeterminada. Imagina que estamos implementando un juego donde, si lanzas un 3 en un lanzamiento de dados, tu jugador no se mueve, sino que obtiene un nuevo sombrero elegante. Si lanzas un 7, tu jugador pierde un sombrero elegante. Para todos los demás valores, tu jugador se mueve esa cantidad de casillas en el tablero de juego. Aquí hay un match que implementa esa lógica, con el resultado del lanzamiento de dados codificado en duro en lugar de un valor aleatorio, y toda la otra lógica representada por funciones sin cuerpo porque en realidad implementarlas está fuera del alcance de este ejemplo:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
  1 other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

Para los primeros dos brazos, los patrones son los valores literales 3 y 7. Para el último brazo que cubre todos los demás valores posibles, el patrón es la variable que hemos elegido llamar other [1]. El código que se ejecuta para el brazo other utiliza la variable pasándola a la función move_player.

Este código se compila, aunque no hayamos enumerado todos los valores posibles que puede tener un u8, porque el último patrón coincidirá con todos los valores no específicamente enumerados. Este patrón genérico cumple con la exigencia de que match debe ser exhaustivo. Tenga en cuenta que tenemos que poner el brazo genérico al final porque los patrones se evalúan en orden. Si ponemos el brazo genérico antes, los otros brazos nunca se ejecutarán, por lo que Rust nos advertirá si agregamos brazos después de un patrón genérico.

Rust también tiene un patrón que podemos usar cuando queremos un patrón genérico pero no queremos usar el valor en el patrón genérico: _ es un patrón especial que coincide con cualquier valor y no se enlaza a ese valor. Esto le dice a Rust que no vamos a usar el valor, por lo que Rust no nos advertirá sobre una variable no utilizada.

Cambiemos las reglas del juego: ahora, si lanzas cualquier cosa que no sea un 3 o un 7, debes volver a lanzar. Ya no necesitamos usar el valor genérico, por lo que podemos cambiar nuestro código para usar _ en lugar de la variable llamada other:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

Este ejemplo también cumple con la exigencia de exhaustividad porque estamos ignorando explícitamente todos los demás valores en el último brazo; no hemos olvidado nada.

Finalmente, cambiaremos las reglas del juego una vez más para que no pase nada más en tu turno si lanzas cualquier cosa que no sea un 3 o un 7. Podemos expresarlo usando el valor unitario (el tipo de tupla vacía que mencionamos en "El Tipo de Tupla") como el código que va con el brazo _:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

Aquí, estamos diciendo a Rust explícitamente que no vamos a usar ningún otro valor que no coincida con un patrón en un brazo anterior, y no queremos ejecutar ningún código en este caso.

Hay más sobre patrones y coincidencias que cubriremos en el Capítulo 18. Por ahora, vamos a pasar a la sintaxis if let, que puede ser útil en situaciones donde la expresión match es un poco verbosa.

Resumen

¡Felicidades! Has completado el laboratorio de la Estructura de Control de Flujo match. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.