Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fluxo de Controle

A capacidade de executar algum código dependendo de uma condição ser true e a capacidade de executar código repetidamente enquanto uma condição for true são blocos fundamentais da maioria das linguagens de programação. As construções mais comuns que permitem controlar o fluxo de execução de código em Rust são expressões if e loops.

Expressões if

Uma expressão if permite ramificar o código dependendo de condições. Você fornece uma condição e então diz: “Se esta condição for satisfeita, execute este bloco de código. Se ela não for satisfeita, não execute esse bloco.”

Crie um novo projeto chamado branches dentro do seu diretório projects para explorar a expressão if. No arquivo src/main.rs, digite o seguinte:

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 de uma condição. Neste caso, a condição verifica se a variável number tem valor menor que 5. Colocamos o bloco de código a ser executado caso a condição seja true logo após a condição, entre chaves. Blocos de código associados às condições em expressões if às vezes são chamados de braços, assim como os braços de expressões match que discutimos na seção “Comparando o Palpite com o Número Secreto” do Capítulo 2.

Opcionalmente, também podemos incluir uma expressão else, como escolhemos fazer aqui, para fornecer ao programa um bloco alternativo de código a ser executado caso a condição avalie para false. Se você não fornecer uma expressão else e a condição for false, o programa simplesmente ignorará o bloco if e seguirá adiante para o próximo trecho de código.

Tente executar esse código; você deverá ver a seguinte saída:

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

Vamos agora alterar o valor de number para um valor que torne a condição false, só para ver o que acontece:

fn main() {
    let number = 7;

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

Execute o programa de novo e veja a saída:

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

Também vale notar que a condição nesse código precisa ser um bool. Se a condição não for um bool, teremos 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");
    }
}

Desta vez, a condição do if avalia para o valor 3, e o Rust gera 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

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

O erro indica que o Rust esperava um bool, mas recebeu um inteiro. Diferentemente de linguagens como Ruby e JavaScript, o Rust não tentará converter automaticamente tipos não booleanos em booleanos. Você precisa ser explícito e sempre fornecer ao if um booleano como condição. Se quisermos que o bloco if execute somente quando um número for diferente de 0, por exemplo, podemos alterar a expressão if para a seguinte forma:

Nome do arquivo: src/main.rs

fn main() {
    let number = 3;

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

Executar esse código imprimirá number was something other than zero.

Tratando Múltiplas Condições com else if

Você pode usar várias 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");
    }
}

Esse programa tem quatro caminhos possíveis. Depois de executá-lo, você deverá ver a seguinte saída:

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

Quando esse programa é executado, ele verifica cada expressão if em ordem e executa o primeiro corpo cuja condição avalia para true. Observe que, mesmo que 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 acontece porque o Rust executa apenas o bloco correspondente à primeira condição true e, depois que encontra uma, nem sequer verifica as demais.

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

Usando if em uma Instrução let

Como if é uma expressão, podemos usá-lo no lado direito de uma instrução let para atribuir o resultado a uma variável, como na Listagem 3-2.

Filename: 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 ficará vinculada a um valor dependendo do resultado da expressão if. Execute esse código para ver o que acontece:

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

Lembre-se de que blocos de código avaliam para a última expressão neles, e números sozinhos também são expressões. Neste caso, o valor da expressão if inteira depende de qual bloco de código é executado. Isso significa que os valores que podem surgir como resultado de cada braço do if precisam ter o mesmo tipo; na Listagem 3-2, os resultados dos braços if e else eram inteiros i32. Se os tipos não forem compatíveis, como no exemplo a seguir, teremos 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 esse código, recebemos um erro. Os braços if e else têm tipos de valor incompatíveis, e o Rust aponta exatamente onde está 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

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

A expressão no bloco if avalia para um inteiro, e a expressão no bloco else avalia para uma string. Isso não funciona porque variáveis precisam ter um único tipo, e o Rust precisa saber de forma definitiva, em tempo de compilação, qual é o tipo da variável number. Conhecer o tipo de number permite que o compilador verifique se esse tipo é válido em todos os lugares em que usamos number. O Rust não conseguiria fazer isso se o tipo de number fosse determinado apenas em tempo de execução; o compilador seria mais complexo e daria menos garantias sobre o código se precisasse acompanhar múltiplos tipos hipotéticos para qualquer variável.

Repetição com Loops

Com frequência é útil executar um bloco de código mais de uma vez. Para isso, o Rust fornece vários loops, que executam o código dentro do corpo do loop até o fim e então voltam imediatamente ao começo. Para experimentar com loops, vamos criar um novo projeto chamado loops.

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

Repetindo Código com loop

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

Como exemplo, altere o arquivo src/main.rs no seu diretório loops para que fique assim:

Nome do arquivo: src/main.rs

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

Quando executarmos esse programa, veremos again! sendo impresso continuamente, repetidas vezes, até interrompermos o programa manualmente. A maioria dos terminais oferece o atalho de teclado ctrl-C para interromper um programa preso em um loop contínuo. Experimente:

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

O símbolo ^C representa o ponto em que você pressionou ctrl-C.

Você pode ou não ver a palavra again! impressa depois de ^C, dependendo de onde o código estava dentro do loop quando recebeu o sinal de interrupção.

Felizmente, o Rust também fornece uma forma de sair de um loop usando código. Você pode colocar a palavra-chave break dentro do loop para dizer ao programa quando deve parar de executá-lo. Lembre-se de que fizemos isso no jogo de adivinhação, na seção “Saindo Depois de um Palpite Correto” do Capítulo 2, para encerrar o programa quando o usuário acertava o número.

Também usamos continue no jogo de adivinhação. Dentro de um loop, essa palavra-chave diz ao programa para pular qualquer código restante da iteração atual e ir direto para a próxima.

Retornando Valores a Partir de Loops

Um dos usos de loop é repetir 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 e usá-lo no restante do código. Para fazer isso, você pode adicionar o valor que deseja retornar após a expressão break usada para interromper o loop; esse valor será retornado para fora do loop, para que você possa utilizá-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 pelo loop. Em cada iteração do loop, somamos 1 à variável counter e então verificamos se counter é igual a 10. Quando isso acontece, usamos a palavra-chave break com o valor counter * 2. Depois do loop, usamos um ponto e vírgula para encerrar a instrução que atribui o valor a result. Por fim, imprimimos o valor contido em result, que neste caso é 20.

Você também pode usar return de dentro de um loop. Enquanto break sai apenas do loop atual, return sempre sai da função atual.

Desambiguando com Rótulos de Loop

Se você tiver loops dentro de loops, break e continue se aplicam ao loop mais interno naquele ponto. Opcionalmente, você pode especificar um rótulo de loop em um loop e então usar esse rótulo com break ou continue para indicar que essas palavras-chave devem se aplicar ao loop rotulado, em vez do mais interno. Rótulos de loop devem começar com uma aspas 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 rótulo, conta regressivamente de 10 a 9. O primeiro break que não especifica rótulo sairá apenas do loop interno. Já a instrução break 'counting_up; sairá do loop externo. Esse código imprime:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [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

Simplificando Loops Condicionais com while

Um programa frequentemente precisa avaliar uma condição dentro de um loop. Enquanto a condição for true, o loop continua. Quando a condição deixa de ser true, o programa chama break, interrompendo o loop. É possível implementar esse comportamento usando uma combinação de loop, if, else e break; se quiser, você pode tentar isso agora em um programa. No entanto, esse padrão é tão comum que o Rust oferece uma construção específica para ele, chamada loop while. Na Listagem 3-3, usamos while para fazer o programa repetir três vezes, contando regressivamente, e então, depois do loop, imprimir uma mensagem e sair.

Filename: src/main.rs
fn main() {
    let mut number = 3;

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

        number -= 1;
    }

    println!("LIFTOFF!!!");
}
Listing 3-3: Usando um loop while para executar código enquanto uma condição avalia para true

Essa construção elimina muito do aninhamento que seria necessário se você usasse loop, if, else e break, além de ficar mais clara. Enquanto a condição avaliar para true, o código é executado; caso contrário, o loop é encerrado.

Percorrendo uma Coleção com for

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

Filename: 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;
    }
}
Listing 3-4: Percorrendo cada elemento de uma coleção usando um loop while

Aqui, o código percorre os elementos do array em ordem crescente. Ele começa no índice 0 e continua até alcançar o índice final do array, isto é, até que index < 5 deixe de ser true. Executar esse código imprimirá todos os elementos do array:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [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

Os cinco valores do array aparecem no terminal, como esperado. Mesmo que index chegue ao valor 5 em algum momento, o loop para antes de tentar buscar um sexto valor do array.

No entanto, essa abordagem é propensa a erros. Poderíamos fazer o programa entrar em pânico se o valor do índice ou a condição do teste estivesse errada. 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. Ela também é mais lenta, porque o compilador insere código de tempo de execução para verificar a cada iteração se o índice está dentro dos limites do array.

Como alternativa mais concisa, você pode usar um loop for e executar algum código para cada item de uma coleção. Um loop for tem a aparência do código na Listagem 3-5.

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
Listing 3-5: Percorrendo cada elemento de uma coleção usando um loop for

Quando executarmos esse código, veremos a mesma saída da Listagem 3-4. Mais importante: aumentamos a segurança do código e eliminamos a chance de bugs resultantes de passar do fim do array ou de não percorrê-lo por completo e deixar elementos para trás. O código de máquina gerado para loops for também pode ser mais eficiente, porque o índice não precisa ser comparado ao comprimento do array em cada iteração.

Usando o loop for, você não precisaria lembrar de alterar outro trecho de código se mudasse a quantidade de valores no array, como acontecia com o método usado na Listagem 3-4.

A segurança e a concisão dos loops for fazem deles a construção de loop mais usada em Rust. Mesmo em situações em que você quer executar algum código um número específico de vezes, como no exemplo da contagem regressiva feito com um loop while na Listagem 3-3, a maioria dos rustaceanos usaria um loop for. O jeito de fazer isso seria usando um Range, fornecido pela biblioteca padrão, que gera todos os números em sequência a partir de um número inicial e até antes de outro número.

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

Nome do arquivo: src/main.rs

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

Esse código fica um pouco melhor, não fica?

Resumo

Você conseguiu! Este foi um capítulo grande: você aprendeu sobre variáveis, tipos de dados escalares e compostos, funções, comentários, expressões if e loops. Para praticar os conceitos discutidos neste capítulo, tente construir programas que façam o seguinte:

  • Converter temperaturas entre Fahrenheit e Celsius.
  • Gerar o enésimo número de Fibonacci.
  • Imprimir a letra da canção natalina “The Twelve Days of Christmas”, aproveitando a repetição da música.

Quando estiver pronto para seguir em frente, vamos falar sobre um conceito do Rust que não existe comumente em outras linguagens de programação: ownership.