Estructuras de control

Se puede ver este capítulo en YouTube en inglés: Parte 1 y Parte 2

Las estructuras de control del flujo de ejecución permiten indicar qué código debe ejecutarse en cada caso. La estructura de control de flujo más simple es if.

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("Es el siete");
    }
}

Se utiliza == y no =. == sirve para comparar y = se utiliza para asignar un valor. También hay que destacar que se escribe if my_number == 7 y no if (my_number == 7). La estructura de control if no necesita paréntesis en Rust.

Esta estructura se completa con else if y else si resultan necesarias:

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("Es el siete");
    } else if my_number == 6 {
        println!("Es el seis")
    } else {
        println!("Es un número diferente")
    }
}

El código anterior imprime Es un númbero diferente porque no es igual a 7 o 6.

Se pueden añadir más condiciones con && (operador y lógico) y || (operador o lógico).

fn main() {
    let my_number = 5;
    if my_number % 2 == 1 && my_number > 0 { // % 2 es el resto de la división entre dos
        println!("Es un número impar positivo");
    } else if my_number == 6 {
        println!("Es el seis")
    } else {
        println!("Es un número diferente")
    }
}

El código anterior imprime Es un número impar positivo porque cuando se divide entre 2 el resto es 1, que es mayor que 0.

Se observa que cuando hay demasiados if, else y else if el código puede resultar difícil de leer. En estos casos (y en otros muchos) se puede utilizar match, que resulta mucho más límpio. match requiere que se contemplen todos los casos posibles para evitar errores. Así que el siguiente código no funciona:

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("Es cero"),
        1 => println!("Es uno"),
        2 => println!("Es dos"),
        // ⚠️
    }
}

El compilar indica lo siguiente:

error[E0004]: non-exhaustive patterns: `3u8..=std::u8::MAX` not covered
 --> src\main.rs:3:11
  |
3 |     match my_number {
  |           ^^^^^^^^^ pattern `3u8..=std::u8::MAX` not covered

El compilador se queja de que solo conoce lo que tiene que ejecutar los casos de 0 a 2, pero u8 puede tener valores hasta el 255 (es decir std::u8::MAX). Qué debe hacer el programa para el resto de valores posibles que pueden aparecer.

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("Es cero"),
        1 => println!("Es uno"),
        2 => println!("Es dos"),
        _ => println!("Es algún otro número"),
    }
}

Este código imprime Es algún otro número.

Para el caso de match hay que recordar que:

  • A todo match le sigue un bloque de código {}
  • Se escriben los patrones a la izquierda y se usa => (flecha gruesa -fat arrow-) para indicar qué hay que hacer cuando hay una coincidencia.
  • A cada línea con un patrón se le denomina "brazo" del match.
  • Entre cada "brazo" se pone una coma de separación (no se usa el punto y coma).

Se puede declarar un valor usando match ya que retorna una valor.

fn main() {
    let my_number = 5;
    let second_number = match my_number {
        0 => 0,
        5 => 10,
        _ => 2,
    };
}

En el ejemplo anterior, second_number tendrá el valor 10. El match, en este caso acaba con un ; ya que una vez se ha finalizado su evaluación esta sentencia es como si se hubiese escrito let second_number = 10;. Que define y asigna el 10 a second_number.

match se puede utilizar para cosas más complejas. Por ejemplo, con tuplas:

fn main() {
    let sky = "nuboso";
    let temperature = "cálido";

    match (sky, temperature) {
        ("nuboso", "frío") => println!("El día es oscuro y desapacible"),
        ("despejado", "cálido") => println!("El día es agradable"),
        ("nuboso", "cálido") => println!("El día es oscuro, pero no se está mal"),
        _ => println!("No sé cómo es el día de hoy"),
    }
}

Este código imprime El día es oscuro, pero no se está mal porque coincide con "nuboso" y "cálido" para sky y temperature.

Incluso se puede utilizar if en las ramas de un match. Es lo que se llama una "guarda de coincidencia" (match guard):

fn main() {
    let children = 5;
    let married = true;

    match (children, married) {
        (children, married) if married == false => println!("Sin casar con {} niños", children),
        (children, married) if children == 0 && married == true => println!("Casado, pero sin niños"),
        _ => println!("¿Casado? {}. Número de niños: {}.", married, children),
    }
}

Este progrma imprimirá ¿Casado? true. Número de niños: 5.

Se puede usar _ tantas veces como se necesite en un match.

fn match_colours(rbg: (i32, i32, i32)) {
    match rbg {
        (r, _, _) if r < 10 => println!("No muy rojo"),
        (_, b, _) if b < 10 => println!("No muy azul"),
        (_, _, g) if g < 10 => println!("No muy verde"),
        _ => println!("Cada color tiene al menos 10"),
    }
}

fn main() {
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colours(first);
    match_colours(second);
    match_colours(third);

}

Este código imprime:

No muy azul
Cada color tiene al menos 10
No muy verde

Este código también muestra cómo funcionan las sentencias match, porque en el primer ejemplo solo imprime No muy rojo, aunque tampoco tiene mucho verde. Las sentencias match siempre se detienen cuando encuentran una coincidencia y no chequea el resto de los "brazos". Es un buen ejemplo de código que compila bien, pero no hace lo que se quiere.

Se puede construir una sentencia matchgigante para arreglar este código, pero probablemente es mejor utilizar un bucle for. Más adelante se hablará de los bucles.

La sentencia match siempre tiene que devolver el mismo tipo de datos en todas sus ramas. Por eso, este código no funciona:

fn main() {
    let my_number = 10;
    let some_variable = match my_number {
        10 => 8,
        _ => "Not ten", // ⚠️
    };
}

El compilador indica lo siguiente:

error[E0308]: `match` arms have incompatible types
  --> src\main.rs:17:14
   |
15 |       let some_variable = match my_number {
   |  _________________________-
16 | |         10 => 8,
   | |               - this is found to be of type `{integer}`
17 | |         _ => "Not ten",
   | |              ^^^^^^^^^ expected integer, found `&str`
18 | |     };
   | |_____- `match` arms have incompatible types

El código siguiente, por la misma razón, tampoco funciona:

fn main() {
    let some_variable = if my_number == 10 { 8 } else { "something else "}; // ⚠️
}

Pero el siguiente código sí funciona, porque no es un matchy son dos sentencias diferentes:

fn main() {
    let my_number = 10;

    if my_number == 10 {
        let some_variable = 8;
    } else {
        let some_variable = "Something else";
    }
}

También se puede usar @ para darle un nombre al valor de un patrón match con el fin de poder usarlo en la expresión correspondiente a ese "brazo". En este ejemplo, se guarda el valor en una variable number para pasarlo a una función. Si es 4 o 13 se usa ese number en la sentencia println!. En otro caso, no se utiliza.

fn match_number(input: i32) {
    match input {
    number @ 4 => println!("{} da mala suerte en China (suena parecido a 死)", number),
    number @ 13 => println!("{} da mala suerte en Norte América, ¡Suerte en Italia! In bocca al lupo", number),
    _ => println!("Es un número normal"),
    }
}

fn main() {
    match_number(50);
    match_number(13);
    match_number(4);
}

Este código imprime:

Es un número normal
13 da mala suerte en Norte América, ¡Suerte en Italia! In bocca al lupo
4 da mala suerte en China (suena parecido a 死)