Fundamentos de Fluxo de Controle em Rust

Beginner

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

Introdução

Bem-vindo ao Fluxo de Controle. Este laboratório faz parte do Livro do Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, focaremos no fluxo de controle em Rust, que envolve o uso de expressões if e loops para executar código com base em condições e para repetir código enquanto uma condição for verdadeira.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 85%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Fluxo de Controle

A capacidade de executar algum código dependendo se uma condição é true e de executar algum código repetidamente enquanto uma condição é true são blocos de construção básicos na maioria das linguagens de programação. As construções mais comuns que permitem controlar o fluxo de execução do código Rust são as expressões if e os loops.

Expressões if

Uma expressão if permite que você ramifique seu código dependendo de condições. Você fornece uma condição e então declara: "Se esta condição for atendida, execute este bloco de código. Se a condição não for atendida, não execute este bloco de código."

Crie um novo projeto chamado branches no seu diretório project para explorar a expressão if. No arquivo src/main.rs, insira o seguinte:

cd ~/project
cargo new branches

Nome do arquivo: src/main.rs

fn main() {
    let number = 3;

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

Todas as expressões if começam com a palavra-chave if, seguida por uma condição. Neste caso, a condição verifica se a variável number tem um valor menor que 5. Colocamos o bloco de código a ser executado se a condição for true imediatamente após a condição dentro de chaves. Blocos de código associados às condições em expressões if são, às vezes, chamados de braços (arms), assim como os braços nas expressões match que discutimos em "Comparando o Palpite ao Número Secreto".

Opcionalmente, também podemos incluir uma expressão else, que escolhemos fazer aqui, para dar ao programa um bloco de código alternativo para executar caso a condição seja avaliada como false. Se você não fornecer uma expressão else e a condição for false, o programa simplesmente pulará o bloco if e passará para o próximo trecho de código.

Tente executar este código; você deve ver a seguinte saída:

$ 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

Vamos tentar mudar o valor de number para um valor que torne a condição false para ver o que acontece:

    let number = 7;

Execute o programa novamente e observe a saída:

$ 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

Também vale a pena notar que a condição neste código deve ser um bool. Se a condição não for um bool, obteremos um erro. Por exemplo, tente executar o seguinte código:

Nome do arquivo: src/main.rs

fn main() {
    let number = 3;

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

A condição if é avaliada como o valor 3 desta vez, e Rust lança um erro:

$ 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

O erro indica que Rust esperava um bool, mas encontrou um inteiro. Ao contrário de linguagens como Ruby e JavaScript, Rust não tentará automaticamente converter tipos não-Booleanos em um Booleano. Você deve ser explícito e sempre fornecer if com um Booleano como sua condição. Se quisermos que o bloco de código if seja executado somente quando um número não for igual a 0, por exemplo, podemos alterar a expressão if para o seguinte:

Nome do arquivo: src/main.rs

fn main() {
    let number = 3;

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

A execução deste código imprimirá number was something other than zero.

Lidando com Múltiplas Condições com else if

Você pode usar múltiplas condições combinando if e else em uma expressão else if. Por exemplo:

Nome do arquivo: 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 tem quatro caminhos possíveis que pode seguir. Após executá-lo, você deve ver a seguinte saída:

$ 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

Quando este programa é executado, ele verifica cada expressão if por sua vez e executa o primeiro corpo para o qual a condição é avaliada como true. Observe que, embora 6 seja divisível por 2, não vemos a saída number is divisible by 2, nem vemos o texto number is not divisible by 4, 3, or 2 do bloco else. Isso ocorre porque Rust executa apenas o bloco para a primeira condição true, e assim que encontra uma, nem sequer verifica o restante.

Usar muitas expressões else if pode sobrecarregar seu código, então, se você tiver mais de uma, pode querer refatorar seu código. O Capítulo 6 descreve uma poderosa construção de ramificação do Rust chamada match para esses casos.

Usando if em uma Declaração let

Como if é uma expressão, podemos usá-la no lado direito de uma declaração let para atribuir o resultado a uma variável, como em Listing 3-2.

Nome do arquivo: src/main.rs

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

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

Listing 3-2: Atribuindo o resultado de uma expressão if a uma variável

A variável number será vinculada a um valor com base no resultado da expressão if. Execute este código para ver o que acontece:

$ 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

Lembre-se que blocos de código são avaliados com base na última expressão neles, e números por si só também são expressões. Neste caso, o valor de toda a expressão if depende de qual bloco de código é executado. Isso significa que os valores que têm o potencial de serem resultados de cada braço (arm) do if devem ser do mesmo tipo; em Listing 3-2, os resultados tanto do braço if quanto do braço else eram inteiros i32. Se os tipos não corresponderem, como no exemplo a seguir, obteremos um erro:

Nome do arquivo: src/main.rs

fn main() {
    let condition = true;

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

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

Quando tentamos compilar este código, obteremos um erro. Os braços if e else têm tipos de valor incompatíveis, e Rust indica exatamente onde encontrar o problema no 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

A expressão no bloco if é avaliada como um inteiro, e a expressão no bloco else é avaliada como uma string. Isso não funcionará porque as variáveis devem ter um único tipo, e Rust precisa saber em tempo de compilação qual é o tipo da variável number, definitivamente. Saber o tipo de number permite que o compilador verifique se o tipo é válido em todos os lugares onde usamos number. Rust não seria capaz de fazer isso se o tipo de number fosse determinado apenas em tempo de execução; o compilador seria mais complexo e faria menos garantias sobre o código se tivesse que acompanhar múltiplos tipos hipotéticos para qualquer variável.

Repetição com Loops

É frequentemente útil executar um bloco de código mais de uma vez. Para esta tarefa, Rust fornece vários loops (laços), que percorrerão o código dentro do corpo do loop até o final e, em seguida, começarão imediatamente de volta ao início. Para experimentar com loops, vamos criar um novo projeto chamado loops.

Rust tem três tipos de loops: loop, while e for. Vamos experimentar cada um.

Repetindo Código com loop

A palavra-chave loop diz ao Rust para executar um bloco de código repetidamente para sempre ou até que você explicitamente diga para parar.

Como exemplo, altere o arquivo src/main.rs no seu diretório loops para que se pareça com isto:

Nome do arquivo: src/main.rs

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

Quando executamos este programa, veremos again! impresso repetidamente e continuamente até pararmos o programa manualmente. A maioria dos terminais suporta o atalho de teclado ctrl-C para interromper um programa que está preso em um loop contínuo. Experimente:

$ 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!

O símbolo ^C representa onde você pressionou ctrl-C. Você pode ou não ver a palavra again! impressa após o ^C, dependendo de onde o código estava no loop quando recebeu o sinal de interrupção.

Felizmente, Rust também fornece uma maneira de sair de um loop usando código. Você pode colocar a palavra-chave break dentro do loop para dizer ao programa quando parar de executar o loop. Lembre-se que fizemos isso no jogo de adivinhação em "Saindo Após um Palpite Correto" para sair do programa quando o usuário vencia o jogo ao adivinhar o número correto.

Também usamos continue no jogo de adivinhação, que em um loop diz ao programa para pular qualquer código restante nesta iteração do loop e ir para a próxima iteração.

Retornando Valores de Loops

Um dos usos de um loop é tentar novamente uma operação que você sabe que pode falhar, como verificar se uma thread concluiu seu trabalho. Você também pode precisar passar o resultado dessa operação para fora do loop para o restante do seu código. Para fazer isso, você pode adicionar o valor que deseja retornar após a expressão break que você usa para parar o loop; esse valor será retornado do loop para que você possa usá-lo, como mostrado aqui:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

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

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

Antes do loop, declaramos uma variável chamada counter e a inicializamos com 0. Em seguida, declaramos uma variável chamada result para armazenar o valor retornado do loop. Em cada iteração do loop, adicionamos 1 à variável counter e, em seguida, verificamos se o counter é igual a 10. Quando é, usamos a palavra-chave break com o valor counter * 2. Após o loop, usamos um ponto e vírgula para finalizar a instrução que atribui o valor a result. Finalmente, imprimimos o valor em result, que neste caso é 20.

Rótulos de Loop para Desambiguar Entre Múltiplos Loops

Se você tiver loops dentro de loops, break e continue se aplicam ao loop mais interno naquele ponto. Você pode, opcionalmente, especificar um rótulo de loop em um loop que você pode então usar com break ou continue para especificar que essas palavras-chave se aplicam ao loop rotulado em vez do loop mais interno. Rótulos de loop devem começar com uma aspa simples. Aqui está um exemplo com dois loops aninhados:

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}");
}

O loop externo tem o rótulo 'counting_up, e ele contará de 0 a 2. O loop interno sem um rótulo conta de 10 a 9. O primeiro break que não especifica um rótulo sairá apenas do loop interno. A instrução break 'counting_up; sairá do loop 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

Loops Condicionais com while

Um programa frequentemente precisará avaliar uma condição dentro de um loop. Enquanto a condição for true, o loop é executado. Quando a condição deixa de ser true, o programa chama break, interrompendo o loop. É possível implementar um comportamento como este usando uma combinação de loop, if, else e break; você pode tentar isso agora em um programa, se desejar. No entanto, esse padrão é tão comum que o Rust tem uma construção de linguagem embutida para isso, chamada de loop while. Na Listagem 3-3, usamos while para executar o programa três vezes, contando regressivamente a cada vez e, em seguida, após o loop, imprimimos uma mensagem e saímos.

Nome do arquivo: src/main.rs

fn main() {
    let mut number = 3;

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

        number -= 1;
    }

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

Listagem 3-3: Usando um loop while para executar código enquanto uma condição é avaliada como true

Essa construção elimina muita aninhamento que seria necessário se você usasse loop, if, else e break, e é mais clara. Enquanto uma condição for avaliada como true, o código é executado; caso contrário, ele sai do loop.

Iterando por uma Coleção com for

Você pode optar por usar a construção while para iterar sobre os elementos de uma coleção, como um array. Por exemplo, o loop na Listagem 3-4 imprime cada elemento no array a.

Nome do arquivo: 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;
    }
}

Listagem 3-4: Iterando por cada elemento de uma coleção usando um loop while

Aqui, o código conta até os elementos no array. Ele começa no índice 0 e, em seguida, itera até atingir o índice final no array (ou seja, quando index < 5 não é mais true). Executar este código imprimirá cada elemento no array:

$ 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

Todos os cinco valores do array aparecem no terminal, como esperado. Embora index atinja um valor de 5 em algum momento, o loop para de executar antes de tentar buscar um sexto valor do array.

No entanto, essa abordagem é propensa a erros; poderíamos fazer com que o programa entrasse em pânico se o valor do índice ou a condição de teste estiverem incorretos. Por exemplo, se você alterasse a definição do array a para ter quatro elementos, mas esquecesse de atualizar a condição para while index < 4, o código entraria em pânico. Também é lento, porque o compilador adiciona código em tempo de execução para realizar a verificação condicional de se o índice está dentro dos limites do array em cada iteração do loop.

Como uma alternativa mais concisa, você pode usar um loop for e executar algum código para cada item em uma coleção. Um loop for se parece com o código na Listagem 3-5.

Nome do arquivo: src/main.rs

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

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

Listagem 3-5: Iterando por cada elemento de uma coleção usando um loop for

Quando executamos este código, veremos a mesma saída que na Listagem 3-4. Mais importante, agora aumentamos a segurança do código e eliminamos a chance de bugs que podem resultar de ir além do final do array ou não ir longe o suficiente e perder alguns itens.

Usando o loop for, você não precisaria se lembrar de alterar nenhum outro código se alterasse o número de valores no array, como faria com o método usado na Listagem 3-4.

A segurança e a concisão dos loops for os tornam a construção de loop mais comumente usada em Rust. Mesmo em situações em que você deseja executar algum código um certo número de vezes, como no exemplo de contagem regressiva que usou um loop while na Listagem 3-3, a maioria dos Rustaceans usaria um loop for. A maneira de fazer isso seria usar um Range, fornecido pela biblioteca padrão, que gera todos os números em sequência, começando de um número e terminando antes de outro número.

Aqui está como a contagem regressiva ficaria usando um loop for e outro método que ainda não discutimos, rev, para reverter o intervalo:

Nome do arquivo: src/main.rs

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

Este código é um pouco melhor, não é?

Resumo

Parabéns! Você concluiu o laboratório de Fluxo de Controle. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.