Aprendendo Rust do zero - Controle de Fluxo 🔀 🔄

Aprendendo Rust do zero - Controle de Fluxo 🔀 🔄

Como ramificar e repetir a execução do código

Introdução 📝

Esse post faz parte de uma série que estou escrevendo chamada "Aprendendo Rust do zero" onde eu tento resumir o que acho ser importante baseado no livro oficial no site do Rust. Nesse aqui gostaria de falar sobre como controlar o fluxo do seu código escrito em Rust utilizando ramificações e repetições.

Sumário 📖

  1. Instalando Rust
  2. Hello, World!
  3. Hello, Cargo!
  4. Variáveis e Mutabilidade
  5. Tipos de dados
  6. Funções
  7. Comentários Simples
  8. Controle de Fluxo - você está aqui

Controle de Fluxo 🔀 🔄

Decidir quando executar ou não um código dependendo de uma condição ser verdadeira ou falsa e quando executar um código repetidamente enquanto uma condição é verdadeira, são os blocos fundamentais na maioria das linguagens de programação. Os constructos mais comuns que permitem que você controle a execução do código feito em Rust são expressões if e loops.

Expressões if 🔀

Uma expressão if permite que você ramifique seu código de acordo com condições. Você define uma condição e uma ação, "Se essa condição for verdadeira, execute esse bloco de código. Se a condição não for verdadeira, não execute esse bloco de código".

Crie um novo projeto chamado branches no seu diretório de projetos para explorar-mos a expressão if. No aquivo src/main.rs, insira o seguinte código:

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, que é seguida por uma condição. Nesse caso, a condição valida se a variável number tem um valor menor que 5 ou não. O bloco de código que nós queremos executar se a condição for verdadeira é colocado imediatamente após a condição dentro de chaves. Blocos de código associados com a condição em uma expressão if são algumas vezes chamadas de braços.

Opicionalmente, nós podemos também incluir uma expressão else, que foi o que nós escolhemos fazer aqui, para dar ao programa um bloco de código alternativo para executar caso o resultado da condição seja falso. Se você não definir uma expressão else e a condição for falsa, o programa irá simplesmente pular o bloco if e mover para o próximo pedaço de código.

Tente executar o código acima, você deverá ver o seguinte resultado:

$ 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 faça a condição ser falsa para vermos o que acontece:

    let number = 7;

Execute o programa novamente, e confira o resultado:

$ 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

É válido notar que a condição nesse código deve ser do tipo bool. Se a condição não for do tipo bool, nós teremos um erro. Por exemplo, tente executar o seguinte código:

fn main() {
    let number = 3;

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

A condição if resolve para o valor 3 dessa vez, e o 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

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

O erro indica que o Rust esperava um boolmas teve um integer. Diferente de linguagens como Ruby e JavaScript, o Rust não irá tentar converter tipos não boolean em um boolean automaticamente. Você deve sempre prover um boolean para ser usado como condição. Se nós queremos que o bloco de código dentro do if somente execute quando um número não é igual a 0, por exemplo, nós podemos mudar a expressão if conforme a seguir:

fn main() {
    let number = 3;

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

Executar o código acima retornará number was something other than zero.

Manipulando múltiplas condições com else if

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

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 posíveis caminhos que ele pode seguir. Depois de executa-lo, você deverá ver o seguinte resultado:

$ 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 esse programa executa, ele verifica cada expressão if por vez e executa o primeiro bloco para o qual a condição é veradeira. Perceba que mesmo 6 sendo divisível por 2, nós não vemos o resultado number is divisible by 2, nem vemos number is not divisible by 4, 3, or 2 do bloco else. Isso acontece porque o Rust só executa o bloco para a primeira condição que for verdadeira, e assim que esse bloco acaba, ele não verifica o restante.

Usar muitas expressões else if pode deixar seu código um pouco bagunçado, caso você tenha mais que um, talvez seja necessário refatorar seu código. Rust tem uma ferramenta poderosa chamada match, que veremos em posts futuros, que pode auxiliar em ramificações complexas no seu código.

Usando if em um statement let

Como if é uma expressão, nós podemos utilizá-lo no lado direito de um statement let, como podemos ver a seguir:

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

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

A variável number irá receber o valor que vier na saída da expressão if. Execute esse código e veja o resultado:

$ 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 retornam a última expressão contida neles e números são por sí só expressões também. Nesse caso, o valor de toda a expressão if depende de qual bloco de código é executado. Isso significa que valores que tem o potencial de ser o resultado de cada ramificação do if deve ser do mesmo tipo. No exemplo acima o resultado de ambos as ramificações do if e do else eram integers i32. Se os tipos não forem os mesmos, como no exemplo a seguir, nós receberemos um erro:

fn main() {
    let condition = true;

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

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

Quando nós tentarmos compilar esse código receberemos um erro. As ramificações if e else tem tipo de valores que são incompatíveis, e o Rust indica exatamente onde encontrar o problema no código:

$ 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` due to previous error

A expressão no bloco do if retorna um integer, e a expressão no bloco do else retorna uma string. Isso não vai funcionar pois variáveis devem ter um único tipo. O Rust precisa saber em tempo de compilação qual o tipo definitivo da variável number, para que ele possa, em tempo de compilação, validar se aquele tipo é válido em todos os lugares onde usamos number. Rust não será capaz de fazer isso se o tipo da variável number só for ser determinado quando for executado. O compilador poderia ser mais complexo, porém faria menos garantias sobre o código se ele tivesse que adivinhar múltiplos tipos hipotéticos de tipos para qualquer variável existente.

Repetição com Loops 🔄

As vezes é útil executar um bloco de código mais de uma vez. Para esse tipo de tarefa, Rust disponibiliza alguns tipos de loops. Um loop executa o código dentro do corpo do loop até o final e então volta imediatamente ao começo. Para vermos como os loops funcionam, vamos criar um novo projeto chamado loops.

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

Repetindo código com loop

A palavra chave loop faz o Rust executar um bloco de código várias e várias vezes para sempre ou até que você explicitamente diga para ele parar.

Como um exemplo, mude o arquivo src/main.rs no seu diretório loops para que fique parecendo com o exemplo a seguir:

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

Quando nós executamos esse programa, nós vermos o texto again! sendo mostrado várias e várias vezes continuamente até que nós paremos o programa manualmente. Muitos terminais oferecem suporte para um atalho de teclado, ctrl-c, para interromper um programa que está preso em um loop. Faça um teste:

$ 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ê apertou ctrl-c. Talvez você não veja a palavra again! aparecendo após ˆC, dependendo de onde o código estava no loop quando o sinal de interromper foi acionado.

Felizmente o Rust disponibiliza uma forma de sair de um loop. Você pode colocar a palavra chave break dentro do loop para dizer ao programa quando parar de executar o loop.

Diferente do break, a palavra chave continue pula o restante do código do loop na iteração atual e vai para a próxima iteração.

Se você tiver loops dentro de loops, break e continue se aplicam ao loop mais interno naquele ponto. Você pode opcionalmente especificar uma label para o loop e então usar a label com break ou continue para que o efeito de break e continue seja sobre o loop com a label em vez do loop mais interno. A seguir um exemplo de 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 de fora tem a label 'counting_up, e ele vai contar de 0 a 2. O loop interno sem uma label conta de 10 a 9. O primeiro break que não define uma label sairá somente do loop interno. O statement break 'counting_up; irá sair do loop externo. Esse código retorna:

$ cargo run
   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

Retornar valores de loops

Um dos usos de um loop é repetir uma operação que você sabe que talvez falhe, como por exemplo, verificar se uma thread completou sua função. Porém, você talvez precise passar o resultado daquela operação para o restante do seu código. Para fazer isso, você pode adicionar o valor que você quer retornar depois do break que você usa para parar o loop. Esse valor será retornado para fora do loop para que você possa utilzá-lo, como podemos ver a seguir:

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, nós declaramos uma variável chamada counter e a inicializamos com 0. Então nós declaramos uma variável chamada result para receber o valor retornado do loop. Em cada iteração do loop, nós adicionamos 1 na variável counter e então verificamos se o contador é igual a 10. Quando for igual, nós usamos o break com o valor counter * 2. Depois do loop, nós usamos um ponto e vírgula para finalizar o statement que atribui o valor à resultado. Finalmente, nós imprimimos o valor contido em result, que no caso é 20.

Loops condicionais com while

As vezes é útil para um programa validar uma condição enquanto faz o loop. Enquanto a condição for verdadeira, o loop executa. Quando a condição para de ser verdadeira, o programa chama um break, parando o loop. Esse tipo de loop pode ser implementado usando uma combinação de loop, if, else e break, você poderia tentar isso agora mesmo em um pograma se quisesse. Entretanto, esse padrão é tão comum que o Rust tem um tipo especial de loop para isso, chamado de while. No exemplo a seguir o programa faz o loop três vezes, fazendo uma contagem regressiva e então depois do loop, imprime outra mensagem e finaliza:

fn main() {
    let mut number = 3;

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

        number -= 1;
    }

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

Esse tipo de loop elimina vários códigos aninhados que seriam necessários se você utilizar loop, if, else e breake é mais limpo. Enquanto a condição for verdadeira, o código executa, caso contrário, o loop finaliza.

Percorrendo uma coleção com for

Você poderia utilizar o loop while para percorrer os elementos de uma coleção, como um array.

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

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

        index += 1;
    }
}

Aqui o código faz uma soma através dos elementos do array. Começa no índice 0 e então itera até que alcance o índice final no array ( que é quando index < 5 não for mais verdadeiro). Executar esse código irá 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. Mesmo que o índice vai alcançar o valor 5 em algum momento, o loop para de executar antes de obter um sexto valor do array.

Mas essa abordagem pode levar a erros, nós poderiamos fazer o programa gerar um panic se o valor do índice ou o teste da condição forem incorretos. É também uma abordagem lenta, pois o compilador adiciona código de tempo de execução para validar condicionalmente se o índice está dentro dos limites do array em cada iteração através do loop.

Como uma forma mais concisa, você pode utilizar um loop for e executar código para cada item de uma coleção. Um loop for se parece com o exemplo a seguir:

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

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

Quando nós executamos esse código, nós veremos o mesmo resultado do exemplo anterior. Mais importante, nós agora aumentamos a segurança do código e eliminamos a chance de bugs que podem aparecer por ir além do fim do array ou não ir longe o suficiente e esquecer alguns itens.

Por exemplo, no código do exemplo anterior, se você mudasse a definição do array a para ter quatro elementos mas esquecesse de atualiar a condição para while index < 4, o código causaria panic. Ao usar o loop for, você não precisaria lembrar de mudar nenhum outro código se você mudasse a quantidade de valores no array.

A segurança e consistência do loop do tipo for faz dele o mais usado em Rust. Mesmo em situações em que você quer executar um código algum número de vezes , como no exemplo de contagem regressíva que usamos um loop while, a maioria dos programadores Rust usariam um loop for. A forma para fazer isso seria usar um Range, que é um tipo disponibilizado pela instalação 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 podemos ver como ficaria a contagem regressiva utilizando um loop for e outra função que não falamos sobre ainda, chamado rev, para reverter o intervalo de números:

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

Esse código parece um pouco mais bonito, não acha?

No próximo post vamos aprender sobre Ownership, a funcionalidade central do Rust.

Referência 📚

Para escrever esse post e os possíveis próximos que virão, estou utilizando o livro online recomendado no site oficial do Rust, que você pode encontrar em doc.rust-lang.org/book/title-page.html.