Fundamentos del Flujo de Control 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 Control Flow. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, nos centraremos en el control de flujo en Rust, que implica el uso de expresiones if y bucles para ejecutar código en función de condiciones y para repetir código mientras una condición sea verdadera.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} rust/mutable_variables -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} rust/boolean_type -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} rust/string_type -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} rust/for_loop -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} rust/function_syntax -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} rust/expressions_statements -.-> lab-100391{{"Fundamentos del Flujo de Control en Rust"}} end

Control de flujo

La capacidad de ejecutar un código determinado según si una condición es true y de ejecutar un código repetidamente mientras una condición es true son bloques básicos de construcción en la mayoría de los lenguajes de programación. Las construcciones más comunes que te permiten controlar el flujo de ejecución del código de Rust son las expresiones if y los bucles.

Expresiones if

Una expresión if te permite bifurcar tu código en función de condiciones. Tú proporcionas una condición y luego se establece: "Si se cumple esta condición, ejecuta este bloque de código. Si la condición no se cumple, no ejecutes este bloque de código".

Crea un nuevo proyecto llamado branches en tu directorio project para explorar la expresión if. En el archivo src/main.rs, escribe lo siguiente:

cd ~/project
cargo new branches

Nombre del archivo: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Todas las expresiones if empiezan con la palabra clave if, seguida de una condición. En este caso, la condición comprueba si la variable number tiene un valor menor que 5. Colocamos el bloque de código a ejecutar si la condición es true inmediatamente después de la condición dentro de llaves. Los bloques de código asociados con las condiciones en las expresiones if a veces se llaman ramas, al igual que las ramas en las expresiones match que discutimos en "Comparando la Adivinanza con el Número Secreto".

Opcionalmente, también podemos incluir una expresión else, que elegimos hacer aquí, para dar al programa un bloque alternativo de código a ejecutar si la condición evalúa a false. Si no proporcionas una expresión else y la condición es false, el programa simplemente saltará el bloque if y pasará al siguiente trozo de código.

Prueba a ejecutar este código; deberías ver la siguiente salida:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

Intentemos cambiar el valor de number a un valor que haga que la condición sea false para ver qué pasa:

    let number = 7;

Ejecuta el programa nuevamente y mira la salida:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

También vale la pena señalar que la condición en este código debe ser un bool. Si la condición no es un bool, obtendremos un error. Por ejemplo, intenta ejecutar el siguiente código:

Nombre del archivo: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

La condición if evalúa a un valor de 3 esta vez, y Rust lanza un error:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

El error indica que Rust esperaba un bool pero recibió un entero. A diferencia de lenguajes como Ruby y JavaScript, Rust no intentará automáticamente convertir tipos no booleanos a un booleano. Debes ser explícito y siempre proporcionar a if un booleano como su condición. Si queremos que el bloque de código if se ejecute solo cuando un número no es igual a 0, por ejemplo, podemos cambiar la expresión if a la siguiente:

Nombre del archivo: src/main.rs

fn main() {
    let number = 3;

    if number!= 0 {
        println!("number was something other than zero");
    }
}

Ejecutar este código imprimirá number was something other than zero.

Manejo de múltiples condiciones con else if

Puedes utilizar múltiples condiciones combinando if y else en una expresión else if. Por ejemplo:

Nombre del archivo: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

Este programa tiene cuatro posibles caminos que puede tomar. Después de ejecutarlo, deberías ver la siguiente salida:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Cuando este programa se ejecuta, comprueba cada expresión if por turnos y ejecuta el primer cuerpo para el cual la condición evalúa a true. Tenga en cuenta que aunque 6 es divisible por 2, no vemos la salida number is divisible by 2, ni tampoco vemos el texto number is not divisible by 4, 3, or 2 del bloque else. Eso se debe a que Rust solo ejecuta el bloque para la primera condición true, y una vez que la encuentra, ni siquiera comprueba el resto.

Utilizar demasiadas expresiones else if puede desordenar su código, por lo que si tiene más de una, es posible que desee refactorizar su código. El Capítulo 6 describe una constructura de ramificación poderosa de Rust llamada match para estos casos.

Usando if en una declaración let

Debido a que if es una expresión, podemos utilizarla en el lado derecho de una declaración let para asignar el resultado a una variable, como en la Lista 3-2.

Nombre del archivo: src/main.rs

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

Lista 3-2: Asignando el resultado de una expresión if a una variable

La variable number se vinculará a un valor basado en el resultado de la expresión if. Ejecute este código para ver qué sucede:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

Recuerde que los bloques de código se evalúan al último expresión en ellos, y los números por sí mismos también son expresiones. En este caso, el valor de la expresión if completa depende de qué bloque de código se ejecuta. Esto significa que los valores que tienen el potencial de ser resultados de cada rama de la if deben ser del mismo tipo; en la Lista 3-2, los resultados de la rama if y la rama else eran enteros i32. Si los tipos no coinciden, como en el siguiente ejemplo, obtendremos un error:

Nombre del archivo: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

Cuando intentamos compilar este código, obtendremos un error. Las ramas if y else tienen tipos de valores incompatibles, y Rust indica exactamente dónde encontrar el problema en el programa:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found
`&str`
  |                                 |
  |                                 expected because of this

La expresión en el bloque if se evalúa a un entero, y la expresión en el bloque else se evalúa a una cadena. Esto no funcionará porque las variables deben tener un solo tipo, y Rust necesita saber en tiempo de compilación qué tipo es la variable number, de manera definitiva. Saber el tipo de number permite al compilador verificar que el tipo es válido en todas partes donde usamos number. Rust no podría hacer eso si el tipo de number solo se determinara en tiempo de ejecución; el compilador sería más complejo y ofrecería menos garantías sobre el código si tuviera que llevar un registro de múltiples tipos hipotéticamente para cualquier variable.

Repetición con Bucles

A menudo es útil ejecutar un bloque de código más de una vez. Para esta tarea, Rust proporciona varios bucles, que ejecutarán el código dentro del cuerpo del bucle hasta el final y luego comenzarán de inmediato nuevamente desde el principio. Para experimentar con los bucles, creemos un nuevo proyecto llamado loops.

Rust tiene tres tipos de bucles: loop, while y for. Intentemos cada uno.

Repitiendo Código con loop

La palabra clave loop le dice a Rust que ejecute un bloque de código una y otra vez para siempre o hasta que le digas explícitamente que pare.

Como ejemplo, cambia el archivo src/main.rs en tu directorio loops para que se vea así:

Nombre del archivo: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Cuando ejecutamos este programa, veremos again! impreso una y otra vez continuamente hasta que detengamos el programa manualmente. La mayoría de las terminales admiten el atajo de teclado ctrl-C para interrumpir un programa que está atascado en un bucle continuo. Prueba:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

El símbolo ^C representa donde presionaste ctrl-C. Puede que veas o no la palabra again! impresa después del ^C, dependiendo de donde estaba el código en el bucle cuando recibió la señal de interrupción.

Afortunadamente, Rust también proporciona una forma de salir de un bucle usando código. Puedes colocar la palabra clave break dentro del bucle para decirle al programa cuándo dejar de ejecutar el bucle. Recuerda que hicimos esto en el juego de adivinanzas en "Saliendo Después de una Adivinanza Correcta" para salir del programa cuando el usuario ganó el juego adivinando el número correcto.

También usamos continue en el juego de adivinanzas, que en un bucle le dice al programa que salte cualquier código restante en esta iteración del bucle y vaya a la siguiente iteración.

Devolviendo Valores desde los Bucles

Una de las usos de un loop es reintentar una operación que sabes que puede fallar, como comprobar si un hilo ha terminado su trabajo. También es posible que necesites pasar el resultado de esa operación fuera del bucle al resto de tu código. Para hacer esto, puedes agregar el valor que quieres devolver después de la expresión break que usas para detener el bucle; ese valor se devolverá fuera del bucle para que puedas usarlo, como se muestra aquí:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Antes del bucle, declaramos una variable llamada counter y la inicializamos a 0. Luego declaramos una variable llamada result para almacenar el valor devuelto por el bucle. En cada iteración del bucle, sumamos 1 a la variable counter, y luego comprobamos si counter es igual a 10. Cuando lo es, usamos la palabra clave break con el valor counter * 2. Después del bucle, usamos un punto y coma para terminar la declaración que asigna el valor a result. Finalmente, imprimimos el valor en result, que en este caso es 20.

Etiquetas de Bucle para Desambiguar Entre Varios Bucles

Si tienes bucles dentro de bucles, break y continue se aplican al bucle más interno en ese momento. Puedes especificar opcionalmente una etiqueta de bucle en un bucle que luego puedes usar con break o continue para especificar que esas palabras clave se aplican al bucle etiquetado en lugar del bucle más interno. Las etiquetas de bucle deben comenzar con una comilla simple. Aquí hay un ejemplo con dos bucles anidados:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

El bucle externo tiene la etiqueta 'counting_up, y contará desde 0 hasta 2. El bucle interno sin etiqueta cuenta hacia abajo desde 10 hasta 9. El primer break que no especifica una etiqueta solo saldrá del bucle interno. La declaración break 'counting_up; saldrá del bucle externo. Este código imprime:

   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

Bucles Condicionales con while

Un programa a menudo necesita evaluar una condición dentro de un bucle. Mientras la condición sea true, el bucle se ejecuta. Cuando la condición deja de ser true, el programa llama a break, deteniendo el bucle. Es posible implementar un comportamiento como este combinando loop, if, else y break; podrías probarlo ahora en un programa, si quieres. Sin embargo, este patrón es tan común que Rust tiene una construcción de lenguaje incorporada para ello, llamada bucle while. En la Lista 3-3, usamos while para hacer que el programa itere tres veces, contando hacia atrás cada vez, y luego, después del bucle, imprimir un mensaje y salir.

Nombre del archivo: src/main.rs

fn main() {
    let mut number = 3;

    while number!= 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Lista 3-3: Usando un bucle while para ejecutar código mientras una condición se evalúa como true

Esta construcción elimina mucha de la anidación que sería necesaria si se usaran loop, if, else y break, y es más clara. Mientras una condición se evalúa como true, el código se ejecuta; de lo contrario, sale del bucle.

Bucle a través de una colección con for

Puedes elegir usar la construcción while para iterar sobre los elementos de una colección, como una matriz. Por ejemplo, el bucle en la Lista 3-4 imprime cada elemento de la matriz a.

Nombre del archivo: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Lista 3-4: Iterando a través de cada elemento de una colección usando un bucle while

Aquí, el código cuenta hacia arriba a través de los elementos de la matriz. Comienza en el índice 0, y luego itera hasta llegar al último índice de la matriz (es decir, cuando index < 5 ya no es true). Ejecutar este código imprimirá cada elemento de la matriz:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

Los cinco valores de la matriz aparecen en la terminal, como se esperaba. Aunque index llegará a un valor de 5 en algún momento, el bucle se detiene antes de intentar obtener un sexto valor de la matriz.

Sin embargo, este enfoque es propenso a errores; podríamos hacer que el programa se detenga abruptamente si el valor del índice o la condición de prueba es incorrecta. Por ejemplo, si cambias la definición de la matriz a para que tenga cuatro elementos pero olvidas actualizar la condición a while index < 4, el código se detendrá abruptamente. También es lento, porque el compilador agrega código en tiempo de ejecución para realizar la comprobación condicional de si el índice está dentro de los límites de la matriz en cada iteración a través del bucle.

Como alternativa más concisa, puedes usar un bucle for y ejecutar un código para cada elemento de una colección. Un bucle for se ve como el código en la Lista 3-5.

Nombre del archivo: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

Lista 3-5: Iterando a través de cada elemento de una colección usando un bucle for

Cuando ejecutamos este código, veremos la misma salida que en la Lista 3-4. Lo más importante es que ahora hemos aumentado la seguridad del código y eliminado la posibilidad de errores que podrían resultar de exceder el final de la matriz o no ir lo suficiente lejos y omitir algunos elementos.

Usando el bucle for, no tendrías que recordar cambiar cualquier otro código si cambias el número de valores en la matriz, como harías con el método usado en la Lista 3-4.

La seguridad y concisión de los bucles for los convierten en la construcción de bucle más comúnmente utilizada en Rust. Incluso en situaciones en las que quieres ejecutar un código un número determinado de veces, como en el ejemplo de cuenta atrás que usó un bucle while en la Lista 3-3, la mayoría de los rustianos usarían un bucle for. La forma de hacer eso sería usar un Range, proporcionado por la biblioteca estándar, que genera todos los números secuencialmente comenzando desde un número y terminando antes de otro número.

Así se vería el cuenta atrás usando un bucle for y otro método que aún no hemos mencionado, rev, para invertir el rango:

Nombre del archivo: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Este código es un poco más bonito, ¿no?

Resumen

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