sexta-feira, 19 de fevereiro de 2021

Conceitos comuns de Programação

Este capítulo cobre os conceitos que aparecem em quase todas as linguagens de programação e como eles funcionam no Rust. Muitas linguagens de programação têm muito em comum em seu núcleo. Nenhum dos conceitos apresentados neste capítulo é exclusivo do Rust, mas vamos discuti-los no contexto do Rust e explicar as convenções sobre o uso desses conceitos.

Especificamente, você aprenderá sobre variáveis, tipos básicos, funções, comentários e fluxo de controle. Essas bases estarão em todos os programas Rust, e aprendê-las desde cedo lhe dará um núcleo forte para começar.

Palavras-chave

A linguagem Rust possui um conjunto de palavras-chave que são reservadas para uso apenas pela linguagem, assim como em outras linguagens. Lembre-se de que você não pode usar essas palavras como nomes de variáveis ou funções. A maioria das palavras-chave tem significados especiais e você as usará para realizar várias tarefas em seus programas Rust; alguns não têm nenhuma funcionalidade atual associada a eles, mas foram reservados para funcionalidades que podem ser adicionadas ao Rust no futuro. Você pode encontrar uma lista de palavras-chave em Apêndice A.

Variáveis e Mutabilidade

Conforme mencionado no Capítulo 2, por padrão, as variáveis são imutáveis. Este é um dos muitos incentivos que o Rust lhe dá para escrever seu código de uma forma que aproveite a segurança e a facilidade de simultaneidade que o Rust oferece. No entanto, você ainda tem a opção de tornar suas variáveis mutáveis. Vamos explorar como e por que Rust o incentiva a favorecer a imutabilidade e por que às vezes você pode querer a mutabilidade.

Quando uma variável é imutável, uma vez que um valor está vinculado a um nome, você não pode alterar esse valor. Para ilustrar isso, vamos gerar um novo projeto chamado variables no diretório de seus projetos usando cargo new variables.

Em seguida, em seu novo diretório variables, abra src/main.rs e substitua seu código pelo seguinte código que ainda não compilou:

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

fn main() {
    let x = 5;
    println!("O valor de x é: {}", x);
    x = 6;
    println!("O valor de x é: {}", x);
}

Salve e execute o programa usando cargo run. Você deve receber uma mensagem de erro, conforme mostrado nesta saída:

>cargo run
   Compiling variables v0.1.0 (C:\Users\user\projetos\variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src\main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: make this binding mutable: `mut x`
3 |     println!("O valor de x é: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables`

To learn more, run the command again with --verbose.
    

Este exemplo mostra como o compilador o ajuda a encontrar erros em seus programas. Mesmo que os erros do compilador possam ser frustrantes, eles apenas significam que seu programa ainda não está fazendo o que você deseja com segurança; eles não significam que você não seja um bom programador! Rustáceos experientes ainda obtêm erros do compilador.

A mensagem de erro indica que a causa do erro é que você cannot assign twice to immutable variable x (não pode atribuir duas vezes à variável imutável x), porque tentou atribuir um segundo valor à variável x que é imutável.

É importante obtermos erros em tempo de compilação quando tentamos alterar um valor que designamos anteriormente como imutável, porque essa mesma situação pode levar a bugs. Se uma parte de nosso código opera na suposição de que um valor nunca mudará e outra parte de nosso código muda esse valor, é possível que a primeira parte do código não faça o que foi projetado para fazer. A causa desse tipo de bug pode ser difícil de rastrear após o fato, especialmente quando a segunda parte do código altera o valor apenas algumas vezes.

No Rust, o compilador garante que, quando você afirma que um valor não muda, ele realmente não muda. Isso significa que, ao ler e escrever código, você não precisa controlar como e onde um valor pode mudar. Seu código é, portanto, mais fácil de raciocinar.

Mas a mutabilidade pode ser muito útil. As variáveis são imutáveis apenas por padrão; como você fez no Capítulo 2, você pode torná-los mutáveis adicionando mut antes do nome da variável. Além de permitir que esse valor seja alterado, mut transmite a intenção a futuros leitores do código, indicando que outras partes do código irão alterar o valor desta variável.

Por exemplo, vamos alterar src/main.rs para o seguinte:

Nome do arquivo: src/main.rs

fn main() {
    let mut x = 5;
    println!("O valor de x é: {}", x);
    x = 6;
    println!("O valor de x é: {}", x);
}

Quando executamos o programa agora, obtemos o seguinte:

>cargo run
   Compiling variables v0.1.0 (C:\Users\user\projetos\variables)
    Finished dev [unoptimized + debuginfo] target(s) in 8.95s
     Running `target\debug\variables.exe`
O valor de x é: 5
O valor de x é: 6
    

Podemos alterar o valor vinculado a x de 5 para 6 quando mut é usado. Em alguns casos, você desejará tornar uma variável mutável porque torna o código mais conveniente de escrever do que se ele tivesse apenas variáveis imutáveis.

Existem várias opções a serem consideradas, além da prevenção de bugs. Por exemplo, nos casos em que você está usando grandes estruturas de dados, transformar uma instância no local pode ser mais rápido do que copiar e retornar instâncias recém-alocadas. Com estruturas de dados menores, criar novas instâncias e escrever em um estilo de programação mais funcional pode ser mais fácil de pensar, portanto, um desempenho inferior pode ser uma penalidade valiosa para obter essa clareza.

Diferenças entre variáveis e constantes

Ser incapaz de alterar o valor de uma variável pode ter lembrado você de outro conceito de programação que a maioria das outras linguagens têm: constantes. Como variáveis imutáveis, constantes são valores vinculados a um nome e não podem ser alterados, mas existem algumas diferenças entre constantes e variáveis.

Primeiro, você não tem permissão para usar mut com constantes. Constantes não são apenas imutáveis por padrão - elas são sempre imutáveis.

Você declara constantes usando a palavra-chave const em vez da palavra-chave let, e o tipo do valor deve ser anotado. Estamos prestes a cobrir os tipos e anotações de tipo na próxima seção, “Tipos de dados”, então não se preocupe com os detalhes agora. Apenas saiba que você deve sempre anotar o tipo.

As constantes podem ser declaradas em qualquer escopo, incluindo o escopo global, o que as torna úteis para valores que muitas partes do código precisam conhecer.

A última diferença é que as constantes podem ser definidas apenas para uma expressão constante, não o resultado de uma chamada de função ou qualquer outro valor que só poderia ser calculado em tempo de execução.

Aqui está um exemplo de declaração de constante onde o nome da constante é MAX_POINTS e seu valor é definido como 100.000. (A convenção de nomenclatura de Rust para constantes é usar todas as letras maiúsculas com sublinhados entre as palavras, e sublinhados podem ser inseridos em literais numéricos para melhorar a legibilidade):

const MAX_POINTS: u32 = 100_000;

As constantes são válidas durante todo o tempo que um programa é executado, dentro do escopo em que foram declaradas, tornando-as uma escolha útil para valores em seu domínio de aplicativo que várias partes do programa podem precisar saber, como o número máximo de pontos que qualquer jogador de um jogo pode ganhar ou a velocidade da luz.

Nomear valores codificados permanentemente usados ​​em todo o seu programa como constantes é útil para transmitir o significado desse valor para futuros mantenedores do código. Também ajuda ter apenas um local em seu código que você precisaria alterar se o valor codificado permanentemente precisasse ser atualizado no futuro.

Como você viu no tutorial do jogo de adivinhação na seção “Comparando a estimativa com o número secreto” no Capítulo 2, você pode declarar uma nova variável com o mesmo nome de uma variável anterior e a nova variável sombreia a variável anterior. Rustáceos dizem que a primeira variável é sombreada pela segunda, o que significa que o valor da segunda variável é o que aparece quando a variável é usada. Podemos sombrear uma variável usando o nome da mesma variável e repetindo o uso da palavra-chave let da seguinte maneira:

Nome do arquivo: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("O valor de x é: {}", x);
}

Este programa primeiro associa x ao valor 5. Em seguida, ele sombreia x repetindo let x =, tomando o valor original e adicionan do 1 de forma que o valor de x é 6. A terceira declaração let também sombreia x, multiplicando o valor anterior de x por 2 para obter um valor final de 12. Quando executamos este programa, ele irá gerar o seguinte:

>cargo run
   Compiling variables v0.1.0 (C:\Users\user\projetos\variables)
    Finished dev [unoptimized + debuginfo] target(s) in 1.81s
     Running `target\debug\variables.exe`
O valor de x é: 12
    

Sombreamento é diferente de marcar uma variável como mut, porque obteremos um erro em tempo de compilação se tentarmos acidentalmente reatribuir um valor a esta variável sem usar a palavra-chave let. Ao usar let, podemos realizar algumas transformações em um valor, mas ter a variável imutável depois que essas transformações forem concluídas.

A outra diferença entre mut e sombreamento é que, como estamos efetivamente criando uma nova variável quando usamos a palavra-chave let novamente, podemos alterar o tipo do valor, mas reutilizar o mesmo nome. Por exemplo, digamos que nosso programa peça a um usuário para mostrar quantos espaços ele deseja entre algum texto inserindo caracteres de espaço, mas realmente queremos armazenar essa entrada como um número:

let espacos = "   ";
let espacos = espacos.len();

Essa construção é permitida porque a primeira variável espacos é um tipo de string e a segunda variável espacos, que é uma variável totalmente nova que por acaso tem o mesmo nome da primeira, é um tipo de número. O sombreamento, portanto, nos poupa de ter que inventar nomes diferentes, como espacos_str e espacos_num; em vez disso, podemos reutilizar o nome espacos mais vezes. No entanto, se tentarmos usar mut para isso, conforme mostrado aqui, obteremos um erro em tempo de compilação:

Esse código não compila Esse código não compila.

let mut espacos = "   ";
        espacos = espacos.len();

O erro diz que não podemos alterar o tipo de uma variável:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     espacos = espacos.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables`

To learn more, run the command again with --verbose.
    

Agora que exploramos como as variáveis funcionam, vamos examinar mais tipos de dados que elas podem ter.

Tipos de dados

Cada valor em Rust é de um determinado tipo de dados, o que informa ao Rust que tipo de dados está sendo especificado para que ele saiba como trabalhar com esses dados. Veremos dois subconjuntos de tipo de dados: escalar e composto.

Lembre-se de que Rust é uma linguagem de tipo estático, o que significa que ela deve conhecer os tipos de todas as variáveis ​​em tempo de compilação. O compilador geralmente pode inferir que tipo queremos usar com base no valor e como o usamos. Nos casos em que muitos tipos são possíveis, como quando convertemos a String em um tipo numérico usando o método parse na seção “Comparando o palpite com o número secreto” no Capítulo 2, devemos adicionar uma anotação de tipo, como esta:

let palpite: u32 = "42".parse().expect("Não é um número!");

Se não adicionarmos a anotação de qual tipo queremos, Rust exibirá o seguinte erro, o que significa que o compilador precisa de mais informações para saber qual tipo queremos usar:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let palpite = "42".parse().expect("Not a number!");
  |         ^^^^^^^ consider giving `guess` a type

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations`

To learn more, run the command again with --verbose.
    

Você verá anotações de tipo diferente para outros tipos de dados.

Tipos escalares

Um tipo escalar representa um único valor. Rust tem quatro tipos escalares primários: inteiros, números de ponto flutuante, booleanos e caracteres. Você pode reconhecê-los de outras linguagens de programação. Vamos ver como eles funcionam no Rust.

Tipos inteiros

Um inteiro é um número sem um componente fracionário. Usamos um tipo inteiro no Capítulo 2, o tipo u32. Esta declaração de tipo indica que o valor ao qual está associado deve ser um inteiro não assinado (tipos inteiros assinados começam com i, em vez de u) que ocupa 32 bits de espaço. A Tabela 3-1 mostra os tipos de inteiros integrados em Rust. Cada variante nas colunas Som sinal e Sem sinal (por exemplo, i16) pode ser usada para declarar o tipo de um valor inteiro.

Tabela 3-1: Tipos inteiros em Rust

Tamanho Com sinal Sem sinal
8 bits i8 u8
16 bits i16 u16
32 bits i32 u32
64 bits i64 u64
128 bits i128 u128
arcos isize usize

Cada variante pode ser com sinal ou não e tem um tamanho explícito. Com sinal e sem sinal referem-se à possibilidade do número ser negativo - em outras palavras, se o número precisa ter um sinal com ele (com sinal) ou se será apenas positivo e, portanto, pode ser representado sem um sinal (Sem sinal). É como escrever números no papel: quando o sinal importa, um número é mostrado com um sinal de mais ou um sinal de menos; entretanto, quando é seguro presumir que o número é positivo, ele é mostrado sem nenhum sinal. Os números com sinal são armazenados usando a representação de complemento de dois.

Cada variante com sinal pode armazenar números de -(2 n - 1 ) a 2 n - 1 - 1 inclusive, onde n é o número de bits que a variante usa. Portanto, um i8 pode armazenar números de -(2 7 ) a 27 - 1, que é igual a -128 a 127. Variantes sem sinal podem armazenar números de 0 a 2 n - 1, então u8 pode armazenar números de 0 a 28 - 1, que é igual a 0 até 255.

Além disso, os tipos isize e usize dependem do tipo de computador em que seu programa está sendo executado: 64 bits se você estiver em uma arquitetura de 64 bits e 32 bits se estiver em uma arquitetura de 32 bits.

Você pode escrever literais inteiros em qualquer uma das formas mostradas na Tabela 3-2. Observe que todos os literais de número, exceto o literal de byte, permitem um sufixo de tipo, como 57u8, e _ como um separador visual, como 1_000.

Tabela 3-2: Literais inteiros em Rust

Literais de número Exemplo
Decimal 98_222
Hexadecimal 0xff
Octal 0o77
Binário 0b1111_0000
Byte (apenas u8) b'A'

Então, como você sabe que tipo de número inteiro usar? Se você não tiver certeza, os padrões do Rust são geralmente boas escolhas, e os tipos inteiros são i32: este tipo é geralmente o mais rápido, mesmo em sistemas de 64 bits. A situação primária em que você usaria isize ou usize é ao indexar algum tipo de coleção.

Estouro de inteiro

Digamos que você tenha uma variável do tipo u8 que pode conter valores entre 0 e 255. Se você tentar alterar a variável para um valor fora desse intervalo, como 256, ocorrerá um estouro de inteiro. Rust tem algumas regras interessantes envolvendo esse comportamento. Quando você está compilando no modo de depuração, o Rust inclui verificações de estouro de inteiros que fazem seu programa entrar em pânico no tempo de execução se esse comportamento ocorrer. Rust usa o termo pânico quando um programa é encerrado com um erro; discutiremos os pânicos com mais detalhes na seção “Erros irrecuperáveis com pânico!” no Capítulo 9.

Quando você está compilando no modo de liberação com a flag --release, o Rust não inclui verificações de estouro de inteiros que causam pânico. Em vez disso, se ocorrer estouro, Rust executa o empacotamento de complemento de dois. Resumindo, valores maiores do que o valor máximo que o tipo pode conter “envolvem” ao mínimo dos valores que o tipo pode conter. No caso de u8, 256 torna-se 0, 257 torna-se 1 e assim por diante. O programa não entrará em pânico, mas a variável terá um valor que provavelmente não é o que você esperava. Depender do comportamento de agrupamento do estouro de inteiro é considerado um erro.

Para lidar explicitamente com a possibilidade de estouro, você pode usar estas famílias de métodos que a biblioteca padrão fornece em tipos numéricos primitivos:

  • Envolva em todos os modos com os métodos wrapping_*, como wrapping_add.
  • Retorne o valor None se houver estouro com os métodos checked_*.
  • Retorna o valor e um booleano indicando se houve estouro com os métodos overflowing_*.
  • Sature nos valores mínimo ou máximo do valor com métodos saturating_*.

Tipos de ponto flutuante

Rust também possui dois tipos primitivos para números de ponto flutuante, que são números com casas decimais. Os tipos de ponto flutuante do Rust são f32 e f64, que têm 32 bits e 64 bits, respectivamente. O tipo padrão é f64 porque em CPUs modernas é quase a mesma velocidade que f32, mas pode ter mais precisão.

Aqui está um exemplo que mostra números de ponto flutuante em ação:

Nome do arquivo: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Os números de ponto flutuante são representados de acordo com o padrão IEEE-754. O tipo f32 é um float de precisão simples e f64 tem precisão dupla.

Operações Numéricas

Rust suporta as operações matemáticas básicas que você esperaria para todos os tipos de número: adição, subtração, multiplicação, divisão e resto. O código a seguir mostra como você usaria cada uma com uma declaração let:

Nome do arquivo: src/main.rs

fn main() {
    // adição
    let sum = 5 + 10;

    // subtração
    let difference = 95.5 - 4.3;

    // multiplicação
    let product = 4 * 30;

    // divisão
    let quotient = 56.7 / 32.2;

    // resto
    let remainder = 43 % 5;
}

Cada expressão nessas instruções usa um operador matemático e é avaliada como um único valor, que é então vinculado a uma variável. O Apêndice B contém uma lista de todos os operadores fornecidos pelo Rust.

O tipo booleano

Como na maioria das outras linguagens de programação, um tipo booleano em Rust tem dois valores possíveis: true e false. Os booleanos têm um byte de tamanho. O tipo booleano em Rust é especificado usando bool. Por exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // Com anotação de tipo explicita
}

A principal forma de usar valores booleanos é por meio de condicionais, como uma expressão if. Abordaremos como as expressões if funcionam no Rust na seção “Fluxo de controle”.

O tipo caractere

Até agora, trabalhamos apenas com números, mas Rust também aceita letras. O tipo char do Rust é o tipo alfabético mais primitivo da linguagem, e o código a seguir mostra uma maneira de usá-lo. (Observe que os literais char são especificados com aspas simples, ao contrário dos literais de string, que usam aspas duplas.)

Nome do arquivo: src/main.rs

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

O tipo char do Rust tem quatro bytes de tamanho e representa um valor escalar Unicode, o que significa que pode representar muito mais do que apenas ASCII. Letras acentuadas; Caracteres chineses, japoneses e coreanos; emoji; e espaços de largura zero são todos valores char válidos em Rust. Os valores Unicode escalares vão desde U+0000a U+D7FFe U+E000para U+10FFFF inclusiva. No entanto, um caractere não é realmente um conceito em Unicode, então sua intuição humana sobre o que é um caractere pode não corresponder ao que char é em Rust. Discutiremos esse tópico em detalhes em “Armazenando Texto Codificado em UTF-8 com Strings” no Capítulo 8.

Tipos compostos

Os tipos compostos podem agrupar vários valores em um tipo. Rust tem dois tipos de compostos primitivos: tuplas e matrizes.

O tipo tupla

Uma tupla é uma maneira geral de agrupar vários valores com uma variedade de tipos em um tipo composto. As tuplas têm um comprimento fixo: uma vez declaradas, elas não podem aumentar ou diminuir de tamanho.

Criamos uma tupla escrevendo uma lista de valores separados por vírgulas entre parênteses. Cada posição na tupla tem um tipo e os tipos dos diferentes valores na tupla não precisam ser os mesmos. Adicionamos anotações de tipo opcionais neste exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

A variável tup se liga a toda a tupla, porque uma tupla é considerada um único elemento composto. Para obter os valores individuais de uma tupla, podemos usar a correspondência de padrões para desestruturar um valor de tupla, como este:

Nome do arquivo: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("O valor de y é: {}", y);
}

Este programa primeiro cria uma tupla e a vincula à variável tup. Em seguida, usa um padrão com let para pegar tup e transformá-lo em três variáveis separadas, x, y, e z. Isso é chamado de desestruturação, porque divide a tupla única em três partes. Finalmente, o programa imprime o valor de y, que é 6.4.

Além da desestruturação por meio de correspondência de padrões, podemos acessar um elemento da tupla diretamente usando um ponto (.) seguido pelo índice do valor que queremos acessar. Por exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let quinhentos = x.0;

    let seis_ponto_quatro = x.1;

    let um = x.2;
}

Este programa cria uma tupla, x, e então cria novas variáveis ​​para cada elemento usando seus respectivos índices. Como acontece com a maioria das linguagens de programação, o primeiro índice em uma tupla é 0.

O tipo array (matriz)

Outra maneira de ter uma coleção de vários valores é com um array. Ao contrário de uma tupla, cada elemento de um array deve ter o mesmo tipo. Arrays em Rust são diferentes de arrays em algumas outras linguagens porque arrays em Rust têm um comprimento fixo, como tuplas.

Em Rust, os valores que entram em uma matriz são escritos como uma lista separada por vírgulas entre colchetes:

Nome do arquivo: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Os arrays são úteis quando você deseja que seus dados sejam alocados na pilha em vez de no heap (discutiremos a pilha e o heap mais no Capítulo 4) ou quando deseja garantir que sempre tenha um número fixo de elementos. No entanto, uma matriz não é tão flexível quanto o tipo de vetor. Um vetor é um tipo de coleção semelhante fornecido pela biblioteca padrão que é deixada a crescer ou diminuir de tamanho. Se você não tiver certeza se deve usar uma matriz ou um vetor, provavelmente deve usar um vetor. O Capítulo 8 discute os vetores com mais detalhes.

Um exemplo de quando você pode querer usar uma matriz em vez de um vetor é em um programa que precisa saber os nomes dos meses do ano. É muito improvável que tal programa precise adicionar ou remover meses, então você pode usar uma matriz porque sabe que sempre conterá 12 elementos:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

Você escreveria o tipo de um array usando colchetes, e entre os colchetes incluiria o tipo de cada elemento, um ponto-e-vírgula e o número de elementos no array, assim:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Aqui i32 é o tipo de cada elemento. Após o ponto-e-vírgula, o número 5 indica que a matriz contém cinco elementos.

Escrever um tipo de array desta forma é semelhante a uma sintaxe alternativa para inicializar um array: se você deseja criar um array que contém o mesmo valor para cada elemento, você pode especificar o valor inicial, seguido por um ponto-e-vírgula e, em seguida, o comprimento do array entre colchetes, conforme mostrado aqui:

let a = [3; 5];

O array nomeado a conterá 5 elementos que serão todos configurados com o valor inicialmente 3. Isso é o mesmo que escrever, let a = [3, 3, 3, 3, 3]; mas de uma forma mais concisa.

Acessando Elementos do array

Um array é um único pedaço de memória alocado na pilha. Você pode acessar elementos de um array usando indexação, como este:

Nome do arquivo: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Neste exemplo, a variável nomeada first obterá o valor 1, porque esse é o valor no índice [0] do array. A variável nomeada second obterá o valor 2 que é o valor no índice [1] do array.

Acesso inválido de elemento de um array

O que acontece se você tentar acessar um elemento de uma matriz que já passou do final da matriz? Digamos que você altere o exemplo para o código a seguir, que será compilado, mas sairá com um erro ao ser executado:

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let elemento = a[index];

    println!("O valor do elemento é: {}", elemento);
}

Executar este código usando cargo run produz o seguinte resultado:

$ cargo run
   Compiling arrays v0.1.0 (file:///projects/arrays)
error: this operation will panic at runtime
 --> src/main.rs:5:19
  |
5 |     let elemento = a[index];
  |                    ^^^^^^^^ index out of bounds: the length is 5 but the index is 10
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to previous error

error: could not compile `arrays`

To learn more, run the command again with --verbose.
    

A compilação não produziu erros, mas o programa resultou em um erro de execução e não foi encerrado com êxito. Quando você tenta acessar um elemento usando indexação, Rust irá verificar se o índice que você especificou é menor que o comprimento do array. Se o índice for maior ou igual ao comprimento da matriz, Rust entrará em pânico.

Este é o primeiro exemplo dos princípios de segurança da Rust em ação. Em muitas linguagens de baixo nível, esse tipo de verificação não é feito e, quando você fornece um índice incorreto, a memória inválida pode ser acessada. Rust protege você contra esse tipo de erro, saindo imediatamente em vez de permitir o acesso à memória e continuar. O Capítulo 9 discute mais sobre o tratamento de erros do Rust.

Funções

As funções são difundidas no código Rust. Você já viu uma das funções mais importantes da linguagem: a função main, que é o ponto de entrada de muitos programas. Você também viu a palavra-chave fn, que permite declarar novas funções.

O código Rust usa o estilo convencional snake case para nomes de funções e variáveis. No caso do snake, todas as letras são minúsculas e sublinhados em palavras separadas. Aqui está um programa que contém uma definição de função de exemplo:

Nome do arquivo: src/main.rs

fn main() {
    println!("Hello, world!");

    outra_funcao();
}

fn outra_funcao() {
    println!("Outra função.");
}

As definições de funções em Rust começam com fn e têm um conjunto de parênteses após o nome da função. As chaves informam ao compilador onde o corpo da função começa e termina.

Podemos chamar qualquer função que definimos inserindo seu nome seguido por um conjunto de parênteses. Como outra_funcao está definido no programa, pode ser chamado de dentro da função main. Observe que definimos outra_funcao após a função main no código-fonte; poderíamos ter definido isso antes também. Rust não se importa onde você define suas funções, apenas se elas estão definidas em algum lugar.

Vamos começar um novo projeto binário denominado funcoes para explorar mais as funções. Coloque o exemplo outra_funcao em src/main.rs e execute-o. Você deve ver a seguinte saída:

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/funcoes`
Hello, world!
Outra função.

As linhas são executadas na ordem em que aparecem na função main. Primeiro, a mensagem "Hello, world! é impressa e, em seguida, outra_funcao é chamada e sua mensagem é impressa.

Parâmetros de Funções

As funções também podem ser definidas para ter parâmetros, que são variáveis ​​especiais que fazem parte da assinatura de uma função. Quando uma função tem parâmetros, você pode fornecer valores concretos para esses parâmetros. Tecnicamente, os valores concretos são chamados de argumentos, mas na conversa casual, as pessoas tendem a usar as palavras parâmetro e argumento de forma intercambiável para as variáveis ​​na definição de uma função ou para os valores concretos passados ​​quando você chama uma função.

A seguinte versão reescrita de outra_funcao mostra a aparência dos parâmetros no Rust:

Nome do arquivo: src/main.rs

fn main() {
    outra_funcao(5);
}

fn outra_funcao(x: i32) {
    println!("O valor de x é: {}", x);
}

Tente executar este programa; você deve obter a seguinte saída:

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/funcoes`
Ovalor de x é: 5

A declaração de outra_funcao tem um parâmetro denominado x. O tipo de x é especificado como i32. Quando 5 é passado para outra_funcao, a macro println! coloca 5 onde o par de chaves estava na string de formato.

Em assinaturas de função, você deve declarar o tipo de cada parâmetro. Esta é uma decisão deliberada no design de Rust: exigir anotações de tipo nas definições de função significa que o compilador quase nunca precisa que você as use em outro lugar no código para descobrir o que você quer dizer.

Quando você quiser que uma função tenha vários parâmetros, separe as declarações dos parâmetros com vírgulas, assim:

Nome do arquivo: src/main.rs

fn main() {
    outra_funcao(5, 6);
}

fn outra_funcao(x: i32, y: i32) {
    println!("O valor de x é: {}", x);
    println!("O valor de y é: {}", y);
}

Este exemplo cria uma função com dois parâmetros, sendo que ambos são do tipo i32. A função então imprime os valores em ambos os seus parâmetros. Observe que os parâmetros da função não precisam ser todos do mesmo tipo, eles apenas são neste exemplo.

Vamos tentar executar este código. Substitua o programa atualmente no arquivo src/main.rs do projeto de funções pelo exemplo anterior e execute-o usando: cargo run

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/funcoes`
O valor de x é: 5
O valor de y é: 6
    

Como chamamos a função com 5 como o valor de x e 6 é passada como o valor de y, as duas strings são impressas com esses valores.

O corpo das funções contêm declarações e expressões

O corpo das funções são compostos de uma série de declarações que terminam opcionalmente em uma expressão. Até agora, cobrimos apenas funções sem uma expressão final, mas você viu uma expressão como parte de uma instrução. Como Rust é uma linguagem baseada em expressões, essa é uma distinção importante a ser entendida. Outras linguagens não têm as mesmas distinções, então vamos ver o que são declarações e expressões e como suas diferenças afetam o corpo das funções.

Na verdade, já usamos declarações e expressões. Declarações são instruções que executam alguma ação e não retornam um valor. As expressões são avaliadas como um valor resultante. Vejamos alguns exemplos.

Criar uma variável e atribuir um valor a ela com a palavra-chave let é uma declaração. Na Listagem 3-1, let y = 6; está uma declaração.

Nome do arquivo: src/main.rs

fn main() {
    let y = 6;
}

Listagem 3-1: Uma declaração de função main contendo uma instrução.

As definições de função também são instruções; todo o exemplo anterior é uma afirmação em si.

As instruções não retornam valores. Portanto, você não pode atribuir uma declaração let a outra variável, como o código a seguir tenta fazer; você obterá um erro:

Nome do arquivo: src/main.rs

fn main() {
    let x = (let y = 6);
}

Ao executar este programa, o erro que você obterá se parecerá com este:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0658]: `let` expressions in this position are experimental
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information

error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^^^^^^^^^^^ help: remove these parentheses
  |
  = note: `#[warn(unused_parens)]` on by default

error: aborting due to 2 previous errors; 1 warning emitted

For more information about this error, try `rustc --explain E0658`.
error: could not compile `functions`

To learn more, run the command again with --verbose.

A declaração let y = 6; não retorna um valor, então não há nada para ser vinculado a x. Isso é diferente do que acontece em outras linguagens, como C e Ruby, onde a atribuição retorna o valor da atribuição. Nessas linguagens, você pode escrever x = y = 6 e ambos x e y ter o valor 6; esse não é o caso em Rust.

As expressões são avaliadas como algo e constituem a maior parte do restante do código que você escreverá em Rust. Considere uma operação matemática simples, como 5 + 6, que é uma expressão que resulta no valor 11. As expressões podem fazer parte das declarações: na Listagem 3-1, o 6 na declaração let y = 6; é uma expressão que resulta no valor 6 atribuido a variavel y. Chamar uma função é uma expressão. Chamar uma macro é uma expressão. O bloco que usamos para criar novos escopos, {},é uma expressão, por exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let x = 5;

    let y = {
        let x = 3;
        x + 1
    };

    println!("O valor de y é: {}", y);
}

Esta expressão:

{
    let x = 3;
    x + 1
}

é um bloco que, neste caso, resulta no valor 4. Esse valor é vinculado a y como parte da declaração let. Observe a linha x + 1 sem ponto-e-vírgula no final, que é diferente da maioria das linhas que você viu até agora. As expressões não incluem ponto e vírgula final. Se você adicionar um ponto-e-vírgula ao final de uma expressão, você o transforma em uma declaração, que não retornará um valor. Lembre-se disso ao explorar os valores e expressões de retorno da função a seguir.

Funções com valores de retorno

As funções podem retornar valores para o código que as chama. Não nomeamos os valores de retorno, mas declaramos seu tipo após uma seta (->). Em Rust, o valor de retorno da função é sinônimo do valor da expressão final no bloco do corpo de uma função. Você pode retornar antecipadamente de uma função usando a palavra-chave return e especificando um valor, mas a maioria das funções retorna a última expressão implicitamente. Aqui está um exemplo de função que retorna um valor:

Nome do arquivo: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("O valor de x é: {}", x);
}

Não há chamadas de função, macros ou mesmo declarações let na função five - apenas o número 5 em si. Essa é uma função perfeitamente válida no Rust. Observe que o tipo de retorno da função também é especificado, como -> i32. Tente executar este código; a saída deve ser semelhante a esta:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
O valor de x é: 5
    

O 5 em five é o valor de retorno da função, e é por isso que o tipo de retorno é i32. Vamos examinar isso com mais detalhes. Existem dois trechos importantes: primeiro, a linha let x = five(); mostra que estamos usando o valor de retorno de uma função para inicializar uma variável. Como a função five retorna um 5, essa linha é igual à seguinte:

let x = 5;

Em segundo lugar, a fivefunção não tem parâmetros e define o tipo do valor de retorno, mas o corpo da função é solitário, 5sem ponto-e-vírgula porque é uma expressão cujo valor queremos retornar.

Vejamos outro exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let x = mais_um(5);

    println!("O valor de x é: {}", x);
}

fn mais_um(x: i32) -> i32 {
    x + 1
}

Executar este código irá imprimir O valor de x é: 6. Mas se colocarmos um ponto-e-vírgula no final da linha que contém x + 1, mudando de uma expressão para uma declaração, obteremos um erro.

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

fn main() {
    let x = mais_um(5);

    println!("O valor de x é: {}", x);
}

fn mais_um(x: i32) -> i32 {
    x + 1;
}

Compilar este código produz um erro, da seguinte maneira:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn mais_um(x: i32) -> i32 {
  |    -------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: consider removing this semicolon

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions`

To learn more, run the command again with --verbose.

A mensagem de erro principal, mismatched types (“tipos incompatíveis”), revela o problema principal com este código. A definição da função mais_um diz que ela retornará um i32, mas as declarações não são avaliadas como um valor, que é expresso por uma tupla vazia (). Portanto, nada é retornado, o que contradiz a definição da função e resulta em um erro. Nesta saída, Rust fornece uma mensagem para possivelmente ajudar a retificar esse problema: ele sugere a remoção do ponto-e-vírgula, o que corrigiria o erro.

Comentários

Todos os programadores se esforçam para tornar seu código fácil de entender, mas às vezes uma explicação extra é necessária. Nesses casos, os programadores deixam notas ou comentários em seu código-fonte que o compilador irá ignorar, mas as pessoas que estão lendo o código-fonte podem achar útil.

Aqui está um comentário simples:

// hello, world

Em Rust, o estilo de comentário idiomático começa um comentário com duas barras, e o comentário continua até o final da linha. Para comentários que vão além de uma única linha, você precisará incluir // em cada linha, assim:

// Então, estamos fazendo algo complicado aqui, longo o suficiente para
// precisarmos de várias linhas de comentários para fazer isso! Uau!
// Esperançosamente, este comentário explicará o que está acontecendo.

Os comentários também podem ser colocados no final das linhas que contêm o código:

Nome do arquivo: src/main.rs

fn main() {
        let numero_da_sorte = 7; // Estou com sorte hoje
    }

Mas você os verá com mais frequência usados neste formato, com o comentário em uma linha separada acima do código que está anotando:

Nome do arquivo: src/main.rs

fn main() {
    // Estou com sorte hoje
    let numero_da_sorte = 7;
}

Rust também tem outro tipo de comentário, comentários de documentação, que discutiremos na seção “Publicando um crate em Crates.io” do Capítulo 14.

Controle de fluxo

Decidir se deve ou não executar algum código dependendo se uma condição é verdadeira e decidir executar algum código repetidamente enquanto uma condição é verdadeira 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 expressões if e loops.

Expressões if

Uma expressão if permite que você ramifique seu código dependendo das condições. Você fornece uma condição e afirma: “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 diretório de seus projetos para explorar a expressão if. No arquivo src/main.rs , insira o seguinte:

Nome do arquivo: src/main.rs

fn main() {
    let numero = 3;

    if numero < 5 {
        println!("A condição era verdadeira");
    } else {
        println!("A condiração era falsa");
    }
}

Todas as expressões if começam com a palavra-chave if, que é seguida por uma condição. Nesse caso, a condição verifica se a variável numero tem ou não um valor menor que 5. O bloco de código que queremos executar se a condição for verdadeira é colocado imediatamente após a condição entre chaves. Os blocos de código associados às condições nas expressões if às vezes são chamados de ramificações, assim como as ramificações nas expressões match que discutimos na seção “Comparando a estimativa com o número secreto” do Capítulo 2.

Opcionalmente, também podemos incluir uma expressão else, que escolhemos fazer aqui, para dar ao programa um bloco alternativo de código para executar caso a condição seja avaliada como falsa. Se você não fornecer uma expressão else e a condição for falsa, o programa irá 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 (C:\Users\user\projetos\branches)
    Finished dev [unoptimized + debuginfo] target(s) in 19.91s
     Running `target\debug\branches.exe`
A condição era verdadeira

Vamos tentar alterar o valor de numero para um valor que crie a condição false para ver o que acontece:

let number = 7;

Execute o programa novamente e observe o resultado:

> cargo run
   Compiling branches v0.1.0 (C:\Users\user\projetos\branches)
    Finished dev [unoptimized + debuginfo] target(s) in 1.85s
     Running `target\debug\branches.exe`
A condiração era falsa

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

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

fn main() {
    let number = 3;

    if number {
        println!("O número era três.");
    }
}

A condição if é avaliada como um valor 3 deste vez, e Rust gera um erro:

> cargo run
   Compiling branches v0.1.0 (C:\Users\user\projetos\branches)
error[E0308]: mismatched types
 --> src\main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

O erro indica que Rust esperava um, bool mas obteve um inteiro. Ao contrário de linguagens como Ruby e JavaScript, o Rust não tentará converter automaticamente tipos não booleanos em booleanos. Você deve ser explícito e sempre fornecer um booleano como condição if. Se quisermos que o bloco de código if seja executado apenas 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!("Número era algo diferente de zero.");
    }
}

Executar este código irá imprimir Número era algo diferente de zero..

Lidando com várias condições com else if

Você pode ter 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!("O número é divisivel por 4");
    } else if number % 3 == 0 {
        println!("O número é divisivel por 3");
    } else if number % 2 == 0 {
        println!("O número é divisivel por 2");
    } else {
        println!("O número não é divisivel por 4, 3, ou 2");
    }
}

Este programa tem quatro caminhos possíveis que pode seguir. Depois de executá-lo, você verá a seguinte saída:

> cargo run
   Compiling branches v0.1.0 (C:\Users\user\projetos\branches)
    Finished dev [unoptimized + debuginfo] target(s) in 27.78s
     Running `target\debug\branches.exe`
O número é divisivel por 3

Quando este programa é executado, ele verifica cada expressão if por vez e executa o primeiro corpo para o qual a condição é verdadeira. Observe que, embora 6 seja divisível por 2, não vemos a saída O número é divisivel por 2, nem vemos o texto O número não é divisivel por 4, 3, ou 2 do bloco else. Isso porque Rust só executa o bloco para a primeira condição verdadeira e, uma vez que encontra uma, nem verifica o resto.

Usar muitas expressões else if pode confundir seu código, portanto, se você tiver mais de uma, talvez queira refatorá-lo. 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

Por ser uma expressão if, podemos usá-la no lado direito de uma declaração let, como na Listagem 3-2.

Nome do arquivo: src/main.rs

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

    println!("O valor da variável 'numero' é: {}", numero);
}

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

A variável numero será associada a um valor baseado no resultado da expressão if. Execute este código para ver o que acontece:

> cargo run
   Compiling branches v0.1.0 (C:\Users\user\projetos\branches)
    Finished dev [unoptimized + debuginfo] target(s) in 8.51s
     Running `target\debug\branches.exe`
O valor da variável 'numero' é: 5

Lembre-se de que os blocos de código avaliam a última expressão neles, e os números por si só também são expressões. Nesse caso, o valor de toda a expressão depende de qual bloco de código é executado. Isso significa que os valores que têm o potencial de ser resultados de cada ramificação do if devem ser do mesmo tipo; na Listagem 3-2, os resultados da ramificação if e da ramificação else eram inteiros i32. Se os tipos forem incompatíveis, como no exemplo a seguir, obteremos um erro:

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

fn main() {
    let condicao = true;

    let numero = if condicao { 5 } else { "seis" };

    println!("O valor da variável 'numero' é: {}", numero);
}

Quando tentarmos compilar este código, obteremos um erro. As ramificações if e else têm valor de tipos incompatíveis e Rust indica exatamente onde encontrar o problema no programa:

> cargo run
   Compiling branches v0.1.0 (C:\Users\user\projetos\branches)
error[E0308]: `if` and `else` have incompatible types
 --> src\main.rs:4:43
  |
4 |     let numero = if condicao { 5 } else { "seis" };
  |                                -          ^^^^^^ expected integer, found `&str`
  |                                |
  |                                expected because of this

error: aborting due to previous error

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

To learn more, run the command again with --verbose.
    

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. Rust precisa saber em tempo de compilação qual é o tipo da variável numero, definitivamente, para que possa verificar em tempo de compilação se seu tipo é válido em todos os lugares que usamos numero. Rust não seria capaz de fazer isso se o tipo de numero só fosse determinado em tempo de execução; o compilador seria mais complexo e ofereceria menos garantias sobre o código se tivesse que controlar vários tipos hipotéticos para qualquer variável.

Repetição com loops

Geralmente, é útil executar um bloco de código mais de uma vez. Para esta tarefa, o Rust fornece vários loops. Um loop percorre o código dentro do corpo do loop até o fim e então começa imediatamente de volta ao início. Para experimentar loops, vamos fazer 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 indefinidamente ou até que você diga explicitamente para parar.

Por exemplo, altere o arquivo src/main.rs em seu diretório loops para ficar assim:

Nome do arquivo: src/main.rs

fn main() {
    loop {
        println!("De novo!");
    }
}

Quando executamos este programa, veremos a impressão De novo! continuamente até interrompermos o programa manualmente. A maioria dos terminais oferece suporte a um atalho de teclado, ctrl-c, para interromper um programa que está travado em um loop contínuo. De uma chance:

> cargo run
   Compiling loops v0.1.0 (C:\Users\user\projetos\loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.84s
     Running `target\debug\loops.exe`
De novo!
De novo!
De novo!
De novo!
error: process didn't exit successfully: `target\debug\loops.exe` (exit code: 0xc000013a, STATUS_CONTROL_C_EXIT)

Felizmente, o Rust fornece outra maneira mais confiável de quebrar um loop. Você pode colocar a palavra-chave break dentro do loop para informar ao programa quando interromper a execução do loop. Lembre-se de que fizemos isso no jogo de adivinhação na seção “Encerrando após uma estimativa correta” do Capítulo 2 para sair do programa quando o usuário venceu o jogo adivinhando o número correto.

Retornando valores de Loops

Um dos usos da declaração loop é tentar novamente uma operação que você sabe que pode falhar, como verificar se um encadeamento concluiu seu trabalho. No entanto, pode ser necessário passar o resultado dessa operação para o resto 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 usá-lo, conforme mostrado aqui:

fn main() {
    let mut contador_de_loop = 0;

    let resultado = loop {
        contador_de_loop += 1;

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

    println!("O resultado é {}", resultado);
}

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

Loops condicionais com while

Geralmente, é útil para um programa avaliar uma condição dentro de um loop. Enquanto a condição for verdadeira, o loop é executado. Quando a condição deixa de ser verdadeira, o programa chama break, interrompendo o loop. Este tipo de circuito pode ser implementado utilizando uma combinação de loop, if, else, e break; você pode tentar isso agora em um programa, se quiser.

No entanto, esse padrão é tão comum que Rust tem uma construção de linguagem embutida para ele, chamada de loop while. A Listagem 3-3 usa while: o programa faz um loop três vezes, contando cada vez, e então, após o loop, ele imprime outra mensagem e sai.

Nome do arquivo: src/main.rs

fn main() {
    let mut numero = 3;

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

        numero -= 1;
    }

    println!("SAINDO!!!");
}

Listagem 3-3: Usando um loop while para executar código enquanto uma condição for verdadeira

Esta construção elimina um monte de nidificação que seria necessário se você usou loop, if, else, e break, e é mais clara. Embora uma condição seja verdadeira, o código é executado; caso contrário, ele sai do loop.

Loop por meio de uma coleção com for

Você pode usar a construção while para percorrer os elementos de uma coleção, como um array. Por exemplo, vamos dar uma olhada na Listagem 3-4.

Nome do arquivo: src/main.rs

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

    while index < 5 {
        println!("O valor é: {}", a[index]);

        index += 1;
    }
}

Listagem 3-4: Loop através de cada elemento de uma coleção usando o loop while

Aqui, o código faz a contagem progressiva dos elementos do array. Ele começa no índice 0 e depois faz um loop até atingir o índice final na matriz (ou seja, quando a expressão index < 5 não é mais verdadeira). Executar este código imprimirá todos os elementos da matriz:

> cargo run
   Compiling loops v0.1.0 (C:\Users\user\projetos\loops)
    Finished dev [unoptimized + debuginfo] target(s) in 1.52s
     Running `target\debug\loops.exe`
O valor é: 10
O valor é: 20
O valor é: 30
O valor é: 40
O valor é: 50
    

Todos os cinco valores da matriz aparecem no terminal, conforme o esperado. Mesmo que index atinja o valor 5 em algum ponto, a execução do loop é interrompida antes de tentar obter um sexto valor da matriz.

Mas essa abordagem está sujeita a erros; podemos fazer com que o programa entre em pânico se o comprimento do índice estiver incorreto. Também é lento, porque o compilador adiciona código de tempo de execução para realizar a verificação condicional em cada elemento em cada iteração através do loop.

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

Nome do arquivo: src/main.rs

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

    for elemento in a.iter() {
        println!("O valor é: {}", elemento);
    }
}

Listagem 3-5: Loop através de cada elemento de uma coleção usando um loop for

Quando executamos esse código, veremos a mesma saída da 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.

Por exemplo, no código da Listagem 3-4, se você alterasse a definição da a array para ter quatro elementos, mas se esquecesse de atualizar a condição para while index < 4, o código entraria em pânico. Usando o loop for, você não precisaria se lembrar de alterar nenhum outro código se alterasse o número de valores na matriz.

A segurança e a concisão dos loops for os tornam a construção de loop mais comumente usada no 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 Rustáceos usaria um loop for. A maneira de fazer isso seria usar um Range, que é um tipo 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.

Esta é a aparência da contagem regressiva usando um loop for e outro método sobre o qual ainda não falamos, rev, para inverter o intervalo:

Nome do arquivo: src/main.rs

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

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

Resumo

Você conseguiu! Esse foi um capítulo considerável: você aprendeu sobre variáveis, tipos de dados escalares e compostos, funções, comentários, expressões if e loops! Se você quiser praticar com os conceitos discutidos neste capítulo, tente construir programas para fazer o seguinte:

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

Quando você estiver pronto para seguir em frente, falaremos sobre um conceito no Rust que normalmente não existe em outras linguagens de programação: propriedade.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

quinta-feira, 18 de fevereiro de 2021

Programando um jogo de adivinhação em Rust

Vamos pular para o Rust trabalhando juntos em um projeto prático! Este capítulo apresenta alguns conceitos comuns do Rust, mostrando como usá-los em um programa real. Você aprenderá sobre let, match, métodos, funções associadas, uso de crates externos e muito mais! Os capítulos seguintes explorarão essas idéias com mais detalhes. Neste capítulo, você praticará os fundamentos.

Implementaremos um problema clássico de programação para iniciantes: um jogo de adivinhação. Funciona assim: o programa irá gerar um número inteiro aleatório entre 1 e 100. Em seguida, ele solicitará que o jogador insira uma estimativa. Depois de inserir uma estimativa, o programa indicará se a estimativa é muito baixa ou muito alta. Se o palpite estiver correto, o jogo irá imprimir uma mensagem de parabéns e sair.

Configurando um Novo Projeto

Para configurar um novo projeto, vá para o diretório de projetos que você criou no Capítulo 1 e faça um novo projeto usando Cargo, assim:

$ cargo new guessing_game
$ cd guessing_game

O primeiro comando, cargo new leva o nome do projeto (guessing_game) como o primeiro argumento. O segundo comando muda para o diretório do novo projeto.

Veja o arquivo Cargo.toml gerado:

Nome do arquivo: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Se as informações do autor que o Cargo obteve do seu ambiente não estiverem corretas, corrija-as no arquivo e salve-o novamente.

Como você viu no Capítulo 1, cargo new gera um programa "Hello, world!" para você. Verifique o arquivo src/main.rs:

Nome do arquivo: src/main.rs

fn main() {
    println!("Hello, world!");
}

Agora vamos compilar este programa Hello, world! e executa-lo na mesma etapa usando o comando cargo run:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
        Running `target/debug/guessing_game`
Hello, world!

O comando run é útil quando você precisa iterar rapidamente em um projeto, como faremos neste jogo, testando rapidamente cada iteração antes de passar para a próxima.

Reabra o arquivo src/main.rs. Você escreverá todo o código neste arquivo.

Processando um palpite

A primeira parte do programa jogo de adivinhação solicitará a entrada do usuário, processará essa entrada e verificará se a entrada está na forma esperada. Para começar, vamos permitir que o jogador dê um palpite. Digite o código na Listagem 2-1 em src/main.rs.

Nome do arquivo: src/main.rs

use std::io;

fn main() {
    println!("Adivinhe o número!");

    println!("Por favor entre com o seu palpite.");

    let mut palpite = String::new();

    io::stdin()
        .read_line(&mut palpite)
        .expect("Falha ao ler a linha.");

    println!("Seu palpite: {}", palpite);
}

Listagem 2-1: Código que obtém um palpite do usuário e a imprime

Este código contém muitas informações, então vamos examiná-lo linha por linha. Para obter a entrada do usuário e, em seguida, imprimir o resultado como saída, precisamos trazer a biblioteca io (entrada/saída) para o escopo. A biblioteca io vem da biblioteca padrão (conhecida como std):

use std::io;

Por padrão, Rust traz apenas alguns tipos no âmbito de cada programa no prelúdio. Se um tipo que você deseja usar não está no prelúdio, você deve trazer esse tipo para o escopo explicitamente com uma declaração use. O uso da biblioteca std::io fornece vários recursos úteis, incluindo a capacidade de aceitar entradas do usuário.

Como você viu no Capítulo 1, a função main é o ponto de entrada para o programa:

fn main() {

A sintaxe fn declara uma nova função, os parênteses, (), indicam que não há parâmetros, e a chave, {, inicia o corpo da função.

Como você também aprendeu no Capítulo 1, println! é uma macro que imprime uma string na tela:

println!("Adivinhe o número!");

println!("Por favor entre com o seu palpite.");

Este código está imprimindo um prompt informando o que é o jogo e solicitando a entrada do usuário.

Armazenamento de valores com variáveis

A seguir, criaremos um local para armazenar a entrada do usuário, como este:

let mut palpite = String::new();

Agora o programa está ficando interessante! Há muita coisa acontecendo nesta pequena linha. Observe que esta é uma instrução let, que é usada para criar uma variável. Aqui está outro exemplo:

let foo = bar;

Esta linha cria uma nova variável chamada foo e a vincula ao valor da variável bar. No Rust, as variáveis são imutáveis por padrão. Discutiremos esse conceito em detalhes na seção “Variáveis e mutabilidade” no Capítulo 3. O exemplo a seguir mostra como usar mut antes do nome da variável para tornar uma variável mutável:

let foo = 5; // imutável
let mut bar = 5; // mutável

Nota: // inicia um comentário que continua até o final da linha. Rust ignora tudo nos comentários, que são discutidos com mais detalhes no Capítulo 3.

Voltemos ao programa de jogo de adivinhação. Agora você sabe que let mut palpite introduzirá uma variável mutável chamada palpite. Do outro lado do sinal de igual (=) está o valor vinculado a palpite, que é o resultado da chamada String::new, uma função que retorna uma nova instância de String. String é um tipo de string fornecido pela biblioteca padrão que é um bit de texto codificado em UTF-8 que pode ser ampliado.

A sintaxe :: na linha ::new indica que new é uma função associada do tipo String. Uma função associada é implementada em um tipo, neste caso String, ao invés de em uma instância particular de String. Algumas linguagens chamam isso de método estático.

Esta função new cria uma nova string vazia. Você encontrará uma função new em muitos tipos, porque é um nome comum para uma função que cria um novo valor de algum tipo.

Para resumir, a linha let mut palpite = String::new(); criou uma variável mutável que está atualmente associada a uma nova instância vazia de String. Uau!

Lembre-se de que incluímos a funcionalidade de entrada/saída da biblioteca padrão use std::io; na primeira linha do programa. Agora vamos chamar a função stdin do módulo io:

io::stdin()
    .read_line(&mut palpite)

Se não tivéssemos colocado a linha use std::io no início do programa, poderíamos ter escrito essa chamada de função como std::io::stdin. A função stdin retorna uma instância de std::io::Stdin, que é um tipo que representa um identificador para a entrada padrão do seu terminal.

A próxima parte do código, .read_line(&mut palpite) chama o método read_line no identificador de entrada padrão para obter a entrada do usuário. Também estamos passando um argumento para read_line: &mut palpite.

O trabalho de read_line é pegar tudo o que o usuário digitar na entrada padrão e colocá-lo em uma string, de modo que essa string seja um argumento. O argumento da string precisa ser mutável para que o método possa alterar o conteúdo da string adicionando a entrada do usuário.

& indica que esse argumento é uma referência, o que fornece uma maneira de permitir que várias partes do seu código acessem um dado sem a necessidade de copiar esses dados para a memória várias vezes. As referências são um recurso complexo e uma das principais vantagens do Rust é o quão seguro e fácil é usar as referências. Você não precisa saber muitos desses detalhes para terminar este programa. Por enquanto, tudo que você precisa saber é que, como as variáveis, as referências são imutáveis ​​por padrão. Portanto, você precisa escrever &mut palpite em vez de &palpite para torná-lo mutável. (O Capítulo 4 explicará as referências mais detalhadamente.)

Lidando com a falha potencial com o tipo Result

Ainda estamos trabalhando nesta linha de código. Embora agora estejamos discutindo uma terceira linha de texto, ela ainda faz parte de uma única linha lógica de código. A próxima parte é este método:

.expect("Falha ao ler a linha.");

Quando você chama um método com a sintaxe .foo(), geralmente é aconselhável introduzir uma nova linha e outro espaço em branco para ajudar a quebrar as linhas longas. Poderíamos ter escrito este código como:

io::stdin().read_line(&mut palpite).expect("Falha ao ler a linha.");

No entanto, uma linha longa é difícil de ler, por isso é melhor dividi-la. Agora vamos discutir o que esta linha faz.

Conforme mencionado anteriormente, read_line coloca o que o usuário digitar na string que estamos passando como parametro para a função, mas também retorna um valor - neste caso, um io::Result. Rust tem vários tipos nomeados Result em sua biblioteca padrão Result versões genéricas e específicas para submódulos, como io::Result.

Os tipos Result são enumerações, geralmente chamadas de enums. Uma enumeração é um tipo que pode ter um conjunto fixo de valores, e esses valores são chamados de variantes enum. O Capítulo 6 abordará enums com mais detalhes.

Pois Result, as variantes são Ok ou Err. A variante Ok indica que a operação foi bem-sucedida e dentro de Ok está o valor gerado com sucesso. A variante Err significa que a operação falhou e Err contém informações sobre como ou por que a operação falhou.

O objetivo desses tipos Result é codificar informações de tratamento de erros. Valores do tipo Result, como valores de qualquer tipo, têm métodos definidos neles. Uma instância de io::Result possui um método expect que você pode chamar. Se esta instância de io::Result for um valor Err, expect fará com que o programa trave e exiba a mensagem que você transmitiu como argumento expect. Se o método read_line retornar um Err, provavelmente será o resultado de um erro proveniente do sistema operacional subjacente. Se esta instância de io::Result for um valor Ok, expect assumirá o valor de retorno que Ok está mantendo e retorna apenas esse valor para que você possa usá-lo. Nesse caso, esse valor é o número de bytes em que o usuário inseriu na entrada padrão.

Se você não chamar expect, o programa será compilado, mas você receberá um aviso:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `std::result::Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut palpite);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
    

Rust avisa que você não usou o valor Result retornado pelo método read_line, indicando que o programa não tratou um possível erro.

A maneira correta de suprimir o aviso é escrever o tratamento de erros, mas como você deseja apenas travar este programa quando ocorrer um problema, você pode usar expect. Você aprenderá a se recuperar de erros no Capítulo 9.

Impressão de valores com marcadores de posição println!

Além da chave de fechamento, há apenas mais uma linha para discutir no código adicionado até agora, que é o seguinte:

println!("Seu palpite: {}", palpite);

Esta linha imprime a string em que salvamos a entrada do usuário. O conjunto de chaves, {}, é um espaço reservado: pense em pequenas pinças de caranguejo que mantêm um valor no lugar. Você pode imprimir mais de um valor usando chaves: o primeiro conjunto de chaves contém o primeiro valor listado após a string de formato, o segundo conjunto contém o segundo valor e assim por diante. Imprimir vários valores em uma chamada para println! ficaria assim:

let x = 5;
let y = 10;

println!("x = {} e y = {}", x, y);

Este código vai imprimir x = 5 e y = 10.

Testando a Primeira Parte

Vamos testar a primeira parte do jogo de adivinhação. Execute-o usando cargo run:

C:\Users\user\projetos\guessing_game>cargo run
   Compiling guessing_game v0.1.0 (C:\Users\user\projetos\guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 22.74s
     Running `target\debug\guessing_game.exe`
Adivinhe o número!
Por favor entre com o seu palpite.
9
Seu palpite: 9
    

Neste ponto, a primeira parte do jogo está concluída: estamos recebendo dados do teclado e depois imprimindo.

Gerando um Número Secreto

Em seguida, precisamos gerar um número secreto que o usuário tentará adivinhar. O número secreto deve ser diferente a cada vez, para que seja divertido jogar mais de uma vez. Vamos usar um número aleatório entre 1 e 100 para que o jogo não seja muito difícil. Rust ainda não inclui a funcionalidade de número aleatório em sua biblioteca padrão. No entanto, a equipe Rust fornece o crate rand.

Usando um crate para obter mais funcionalidade

Lembre-se de que um crate é uma coleção de arquivos de código-fonte do Rust. O projeto que estamos construindo é um crate binário, que é um executável. O crate rand é uma crate de biblioteca, que contém código destinado a ser usado em outros programas.

O uso de crates externos pelo cargo é onde ele realmente brilha. Antes que possamos escrever o código que usa rand, precisamos modificar o arquivo Cargo.toml para incluir o crate rand como uma dependência. Abra esse arquivo agora e adicione a seguinte linha na parte inferior, abaixo do cabeçalho [dependencies] da seção que o Cargo criou para você:

Nome do arquivo: Cargo.toml

[dependencies]
rand = "0.5.5"

No arquivo Cargo.toml, tudo o que segue um cabeçalho é parte de uma seção que continua até que outra seção seja iniciada. A seção [dependencies] é onde você diz ao Cargo de quais crates externos seu projeto depende e de quais versões desses crates você precisa. Nesse caso, especificaremos o crate rand com o especificador de versão semântica 0.5.5. Cargo entende o Controle de Versão Semântico (às vezes chamado de SemVer), que é um padrão para escrever números de versão. O número 0.5.5 é, na verdade, uma abreviação de ^0.5.5, o que significa qualquer versão que esteja no mínimo 0.5.5 abaixo de 0.6.0. Cargo considera que essas versões têm APIs públicas compatíveis com a versão 0.5.5.

Agora, sem alterar nenhum código, vamos construir o projeto, conforme mostrado na Listagem 2-2.

C:\Users\user\projetos\guessing_game>cargo build
    Updating crates.io index
  Downloaded rand v0.5.6
  Downloaded rand_core v0.3.1
  Downloaded rand_core v0.4.2
  Downloaded winapi v0.3.9
  Downloaded 4 crates (1.4 MB) in 4.38s (largest was `winapi` at 1.2 MB)
   Compiling winapi v0.3.9
   Compiling rand_core v0.4.2
   Compiling rand_core v0.3.1
   Compiling rand v0.5.6
   Compiling guessing_game v0.1.0 (C:\Users\user\projetos\guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2m 18s
    

Listagem 2-2: A saída da execução cargo build após adicionar o crate rand como uma dependência

Você pode ver números de versão diferentes (mas todos eles serão compatíveis com o código, graças ao SemVer!), Linhas diferentes (dependendo do sistema operacional) e as linhas podem estar em uma ordem diferente.

Agora que temos uma dependência externa, o Cargo busca as versões mais recentes de tudo no registro, que é uma cópia dos dados de Crates.io. Crates.io é onde as pessoas no ecossistema Rust publicam seus projetos Rust de código aberto para outros usarem.

Depois de atualizar o registro, o Cargo verifica a seção [dependencies] e baixa todos os crates que você ainda não tiver. Neste caso, apesar de listarmos apenas rand como dependência, Cargo também baixou libc e rand_core, porque rand depende deles para funcionar. Depois de baixar os crates, Rust os compila e então compila o projeto com as dependências disponíveis.

Se você executar imediatamente cargo build novamente sem fazer nenhuma alteração, não obterá nenhuma saída além da linha Finished. O Cargo sabe que já baixou e compilou as dependências e você não alterou nada sobre elas no arquivo Cargo.toml. O Cargo também sabe que você não mudou nada em seu código, então também não o recompila. Sem nada para fazer, ele simplesmente sai.

Se você abrir o arquivo src/main.rs, fizer uma alteração trivial, salvá-lo e compilá-lo novamente, verá apenas duas linhas de saída:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
    

Essas linhas mostram que o Cargo atualiza apenas a construção com sua pequena alteração no arquivo src/main.rs. Suas dependências não mudaram, então Cargo sabe que pode reutilizar o que já baixou e compilou para elas. Ele apenas reconstrói sua parte do código.

Garantindo construções reproduzíveis com o arquivo Cargo.lock

O Cargo tem um mecanismo que garante que você possa reconstruir o mesmo artefato sempre que você ou qualquer outra pessoa criar seu código: o Cargo usará apenas as versões das dependências especificadas até que você indique o contrário. Por exemplo, o que acontecerá se na próxima semana a versão 0.5.6 do crate rand for lançada e contiver uma correção de bug importante, mas também contiver uma regressão que quebrará seu código?

A resposta para esse problema é o arquivo Cargo.lock, que foi criado na primeira vez que você executou cargo builde em seu diretório guessing_game. Quando você constrói um projeto pela primeira vez, o Cargo descobre todas as versões das dependências que atendem aos critérios e as grava no arquivo Cargo.lock. Quando você construir seu projeto no futuro, Cargo verá que o arquivo Cargo.lock existe e usará as versões especificadas lá em vez de fazer todo o trabalho de descobrir as versões novamente. Isso permite que você tenha uma construção reproduzível automaticamente. Em outras palavras, seu projeto permanecerá em 0.5.5 até que você atualize explicitamente, graças ao arquivo Cargo.lock.

Atualizando um crate para obter uma nova versão

Quando você não deseja atualizar um crate, cargo fornece outro comando, update que irá ignorar o arquivo Cargo.lock e descobrir todas as últimas versões que atendem às suas especificações em Cargo.toml. Se funcionar, o Cargo gravará essas versões no arquivo Cargo.lock.

Mas por padrão, o Cargo procurará apenas versões maiores que 0.5.5 e menores que 0.6.0. Se o crate rand lançou duas novas versões 0.5.6 e 0.6.0, você veria o seguinte se executasse cargo update:

$ cargo update
    Updating crates.io index
    Updating rand v0.5.5 -> v0.5.6
    

Neste ponto, você também notaria uma mudança em seu arquivo Cargo.lock, observando que a versão do crate rand que você está usando agora é 0.5.6.

Se você quiser usar a versão do rand 0.6.0 ou qualquer versão da série 0.6.x, terá que atualizar o arquivo Cargo.toml para ficar assim:

[dependencies]
rand = "0.6.0"

Na próxima vez que você executar cargo build, o Cargo atualizará o registro de crates disponíveis e reavaliará seus requisitos rand de acordo com a nova versão que você especificou.

Há muito mais a dizer sobre Cargo e seu ecossistema, que discutiremos no Capítulo 14, mas por enquanto, isso é tudo que você precisa saber. O Cargo facilita a reutilização de bibliotecas, de modo que os Rustáceos são capazes de escrever projetos menores que são montados a partir de vários pacotes.

Gerando um Número Aleatório

Agora que você adicionou crate rand ao arquivo Cargo.toml, vamos começar a usar rand. A próxima etapa é atualizar src/main.rs, conforme mostrado na Listagem 2-3.

Nome do arquivo: src/main.rs

use std::io;
use rand::Rng;

fn main() {
    println!("Adivinhe o número!");

    let numero_secreto = rand::thread_rng().gen_range(1, 101);

    println!("O número secreto é: {}", numero_secreto);

    println!("Por favor entre com o seu palpite.");

    let mut palpite = String::new();

    io::stdin()
        .read_line(&mut palpite)
        .expect("Falha ao ler a linha.");

    println!("Seu palpite: {}", palpite);
}

Listagem 2-3: Adicionando código para gerar um número aleatório

Primeiro, adicione uma linha use: use rand::Rng. A característica Rng define métodos que os geradores de números aleatórios implementam, e essa característica deve estar no escopo para que possamos usar esses métodos. O Capítulo 10 abordará as características em detalhes.

Em seguida, estamos adicionando duas linhas no meio. A função rand::thread_rng nos dará o gerador de número aleatório específico que vamos usar: um que é local para o thread atual de execução e propagado pelo sistema operacional. Em seguida, chamamos o método gen_range no gerador de números aleatórios. Esse método é definido pela característica Rng que colocamos no escopo com a instrução use rand::Rng. O método gen_range recebe dois números como argumentos e gera um número aleatório entre eles. É inclusivo no limite inferior, mas exclusivo no limite superior, portanto, precisamos especificar 1 e 101 para solicitar um número entre 1 e 100.

Nota: Você não saberá apenas quais características usar e quais métodos e funções chamar de uma crate. As instruções para usar uma crate estão na documentação de cada crate. Outro recurso interessante do Cargo é que você pode executar o comando cargo doc --open, que criará a documentação fornecida por todas as suas dependências localmente e abri-la em seu navegador. Se você estiver interessado em outras funcionalidades da crate rand, por exemplo, execute cargo doc --open e clique rand na barra lateral à esquerda.

A segunda linha que adicionamos no meio do código imprime o número secreto. Isso é útil enquanto estamos desenvolvendo o programa para poder testá-lo, mas vamos excluí-lo da versão final. Não será um grande jogo se o programa imprimir a resposta assim que for iniciado!

Tente executar o programa algumas vezes:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/guessing_game`
Adivinhe o número!
O número secreto é: 75
Por favor entre com o seu palpite.
9
Seu palpite: 9

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Adivinhe o número!
O número secreto é: 46
Por favor entre com o seu palpite.
2
Seu palpite: 2
    

Você deve obter diferentes números aleatórios, e todos devem ser números entre 1 e 100. Ótimo trabalho!

Comparando o palpite com o número secreto

Agora que temos a entrada do usuário e um número aleatório, podemos compará-los. Essa etapa é mostrada na Listagem 2-4. Observe que este código ainda não compilará, como explicaremos.

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    // --recorte--

    println!("Seu palpite: {}", palpite);

    match guess.cmp(&numero_secreto) {
        Ordering::Less => println!("Muito baixo!"),
        Ordering::Greater => println!("Muito alto!"),
        Ordering::Equal => println!("Você ganhou!"),
    }
}

Listagem 2-4: Lidando com os possíveis valores de retorno da comparação de dois números

O primeiro novo pedaço aqui é outra instrução use, trazendo um tipo chamado std::cmp::Ordering para o escopo da biblioteca padrão. Como Result, Ordering é outro enum, mas as variantes de Ordering são Less (menor), Greater (maior) e Equal (igual). Esses são os três resultados possíveis quando você compara dois valores.

Em seguida, adicionamos cinco novas linhas na parte inferior que usam o tipo Ordering. O método cmp compara dois valores e pode ser chamado em qualquer coisa que possa ser comparada. Ele faz referência a tudo o que você deseja comparar: aqui está comparando o palpite com o numero_secreto. Em seguida, ele retorna uma variante enum do Ordering que incluímos no escopo com a instrução use. Usamos uma expressão match para decidir o que fazer a seguir com base em qual variante de Ordering foi retornada da chamada de cmp com os valores em palpite e numero_secreto.

Uma expressão match é feita de ramificações. Uma ramificação consiste em um padrão e no código que deve ser executado se o valor dado ao início da expressão match se ajustar ao padrão dessa ramificação. Rust pega o valor dado a match e examina o padrão de cada ramificação sucessivamente. A construção match e os padrões são recursos poderosos no Rust que permitem expressar uma variedade de situações que seu código pode encontrar e certificar-se de lidar com todas elas. Esses recursos serão abordados em detalhes no Capítulo 6 e Capítulo 18, respectivamente.

Vamos examinar um exemplo do que aconteceria com a expressão match usada aqui. Digamos que o usuário digitou 50 e o número secreto gerado aleatoriamente é 38. Quando o código comparar 50 com 38, o método cmp retornará Ordering::Greater, porque 50 é maior que 38. A expressão match obtém o valor Ordering::Greater e começa a verificar o padrão de cada ramificação. Ele examina o padrão da primeira ramificação Ordering::Less, e vê que o valor Ordering::Greater não corresponde a Ordering::Less, então ele ignora o código naquela ramificação e passa para a próxima ramificação. O padrão da próxima ramificação Ordering::Greater, corresponde a Ordering::Greater! O código associado naquela ramificação será executado e impresso Muito alto! na tela. A expressão match termina porque não há necessidade de olhar para a última ramificação neste cenário.

No entanto, o código na Listagem 2-4 ainda não será compilado. Vamos tentar:

$ cargo run
   Compiling guessing_game v0.1.0 (/home/thor/Documentos/Rust/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:23
   |
22 |     match palpite.cmp(&numero_secreto) {
   |                       ^^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game`

To learn more, run the command again with --verbose.
    

O núcleo do erro afirma que existem tipos incompatíveis. Rust tem um sistema de tipo estático forte. No entanto, ele também possui inferência de tipo. Quando escrevemos let mut palpite = String::new(), Rust foi capaz de inferir que palpite deveria ser uma String e não nos fez escrever o tipo. O numero_secreto, por outro lado, é um tipo de número. Alguns tipos de número podem ter um valor entre 1 e 100: i32, um número de 32 bits; u32, um número de 32 bits sem sinal; i64, um número de 64 bits; assim como outros. O padrão de Rust é um i32, que é o tipo de numero_secreto, a menos que você adicione informações de tipo em outro lugar que façam Rust inferir um tipo numérico diferente. O motivo do erro é que Rust não pode comparar uma string e um tipo de número.

Em última análise, queremos converter a String inserida no programa como entrada em um tipo de número real para que possamos compará-lo numericamente ao número secreto. Podemos fazer isso adicionando outra linha ao corpo da função main:

Nome do arquivo: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Adivinhe o número!");

    let numero_secreto = rand::thread_rng().gen_range(1, 101);

    println!("O número secreto é: {}", numero_secreto);

    println!("Por favor entre com o seu palpite.");

    let mut palpite = String::new();

    io::stdin()
        .read_line(&mut palpite)
        .expect("Falha ao ler a linha.");

    let palpite: u32 = palpite.trim().parse().expect("Por favor entre com um número!");

    println!("Seu palpite: {}", palpite);

    match palpite.cmp(&numero_secreto) {
        Ordering::Less => println!("Muito baixo!"),
        Ordering::Greater => println!("Muito alto!"),
        Ordering::Equal => println!("Você ganhou!"),
    }
}

A linha é:

let palpite: u32 = palpite.trim().parse().expect("Por favor entre com um número!");

Criamos uma variável chamada palpite. Mas espere, o programa já não tem uma variável chamada palpite? Sim, mas Rust nos permite obscurecer o valor anterior de palpite com um novo. Este recurso é frequentemente usado em situações nas quais você deseja converter um valor de um tipo para outro. O sombreamento nos permite reutilizar o nome palpite da variável em vez de nos forçar a criar duas variáveis exclusivas, como palpite_str e palpite por exemplo. (O Capítulo 3 cobre o sombreamento com mais detalhes.)

Nos ligamos palpite à expressão palpite.trim().parse(). O palpite na expressão refere-se variável original palpite que foi uma String que recebemos como entrada. O método trim em uma instância String eliminará qualquer espaço em branco no início e no final. Embora u32 possa conter apenas caracteres numéricos, o usuário deve pressionar enter para satisfazer read_line. Quando o usuário pressiona enter, um caractere de nova linha é adicionado à string. Por exemplo, se o usuário digita 5 e presionar enter para entrar, palpite se parece com isso: 5\n. O \n representa “nova linha”, o resultado de pressionar enter. O método trim elimina \n, resultando em apenas 5.

O método parse em strings converte uma string em algum tipo de número. Como esse método podemos converter uma variedade de tipos de número, precisamos informar a Rust o tipo de número exato que queremos usar let palpite: u32. Os dois pontos (:) depois de palpite informar a Rust que anotaremos o tipo da variável. Rust tem alguns tipos de números integrados; o u32 visto aqui é um inteiro sem sinal de 32 bits. É uma boa escolha padrão para um pequeno número positivo. Você aprenderá sobre outros tipos de número no Capítulo 3. Além disso, a anotação u32 neste programa de exemplo e a comparação com numero_secreto significa que Rust inferirá que numero_secreto também deve ser um u32. Portanto, agora a comparação será entre dois valores do mesmo tipo!

A chamada para parse pode facilmente causar um erro. Se, por exemplo, a string continha A👍%, não haveria como convertê-la em um número. Como pode falhar, o método parse retorna um tipo Result, da mesma forma que o método read_line (discutido anteriormente em “Lidando com a falha potencial com o tipo Result”). Vamos tratar Result da mesma maneira, usando o método expect novamente. Se parse retornar um variante Err Result porque não foi possível criar um número a partir da string, a chamada expect travará o jogo e imprimirá a mensagem que fornecemos. Se parse puder converter com sucesso a string em um número, ele retornará a variante Ok de Result e expect o número que desejamos do valor Ok.

Vamos rodar o programa agora!

$ cargo run
   Compiling guessing_game v0.1.0 (/home/thor/Documentos/Rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.67s
     Running `target/debug/guessing_game`
Adivinhe o número!
O número secreto é: 9
Por favor entre com o seu palpite.
8
Seu palpite: 8
Muito baixo!
    

Legal! Mesmo que espaços tenham sido adicionados antes da estimativa, o programa ainda descobriu que o usuário adivinhou 76. Execute o programa algumas vezes para verificar o comportamento diferente com diferentes tipos de entrada: adivinhe o número corretamente, adivinhe um número muito alto, e adivinhe um número muito baixo.

Temos a maior parte do jogo funcionando agora, mas o usuário só pode tentar adivinhar o número uma vez. Vamos mudar isso adicionando um loop!

Permitindo várias tentativas com Looping

A palavra-chave loop cria um loop infinito. Vamos adicionar isso agora para dar aos usuários mais chances de adivinhar o número:

Nome do arquivo: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Adivinhe o número!");

    let numero_secreto = rand::thread_rng().gen_range(1, 101);

    println!("O número secreto é: {}", numero_secreto);

    loop {

        println!("Por favor entre com o seu palpite.");

        let mut palpite = String::new();

        io::stdin()
            .read_line(&mut palpite)
            .expect("Falha ao ler a linha.");

        let palpite: u32 = palpite.trim().parse().expect("Por favor entre com um número!");

        println!("Seu palpite: {}", palpite);

        match palpite.cmp(&numero_secreto) {
            Ordering::Less => println!("Muito baixo!"),
            Ordering::Greater => println!("Muito alto!"),
            Ordering::Equal => println!("Você ganhou!"),
        }
    }
}

Como você pode ver, movemos tudo do prompt de entrada de adivinhação em diante para dentro de loop. Certifique-se de recuar as linhas dentro do loop mais quatro espaços cada e execute o programa novamente. Observe que há um novo problema porque o programa está fazendo exatamente o que dissemos para ele fazer: peça outro palpite para sempre! Não parece que o usuário pode sair!

O usuário sempre pode interromper o programa usando o atalho de teclado ctrl-c. Mas há outra maneira de escapar desse monstro insaciável, conforme mencionado na discussão parse em “Comparando a Suposição com o Número Secreto”: se o usuário digitar uma resposta não numérica, o programa irá travar. O usuário pode tirar vantagem disso para sair, conforme mostrado aqui:

>cargo run
   Compiling guessing_game v0.1.0 (C:\Users\user\projetos\guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 27.11s
     Running `target\debug\guessing_game.exe`
Adivinhe o número!
O número secreto é: 91
Por favor entre com o seu palpite.
76
Seu palpite: 76
Muito baixo!
Por favor entre com o seu palpite.
100
Seu palpite: 100
Muito alto!
Por favor entre com o seu palpite.
91
Seu palpite: 91
Você ganhou!
Por favor entre com o seu palpite.
sair
thread 'main' panicked at 'Por favor entre com um número!: ParseIntError { kind: InvalidDigit }', src\main.rs:22:51
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)
    

Digitando sair realmente fecha o jogo, mas o mesmo acontecerá com qualquer outra entrada não numérica. No entanto, isso não é ideal para dizer o mínimo. Queremos que o jogo pare automaticamente quando o número correto for adivinhado.

Sair após um palpite correto

Vamos programar o jogo para encerrar quando o usuário vencer, adicionando uma declaração break:

Nome do arquivo: src/main.rs

match palpite.cmp(&numero_secreto) {
    Ordering::Less => println!("Muito baixo!"),
    Ordering::Greater => println!("Muito alto!"),
    Ordering::Equal => {
        println!("Você ganhou!");
        break;
    }
}

Adicionar a linha break após Você ganhou! faz com que o programa saia do loop quando o usuário adivinhar o número secreto corretamente. Sair do loop também significa sair do programa, porque o loop é a última parte da função main.

Tratamento de entrada inválida

Para refinar ainda mais o comportamento do jogo, em vez de travar o programa quando o usuário insere um não-número, vamos fazer o jogo ignorar um não-número para que o usuário possa continuar adivinhando. Podemos fazer isso alterando a linha onde palpite é convertida de String para um u32, conforme mostrado na Listagem 2-5.

Nome do arquivo: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Adivinhe o número!");

    let numero_secreto = rand::thread_rng().gen_range(1, 101);

    println!("O número secreto é: {}", numero_secreto);

    loop {

        println!("Por favor entre com o seu palpite.");

        let mut palpite = String::new();

        io::stdin()
            .read_line(&mut palpite)
            .expect("Falha ao ler a linha.");

        let palpite: u32 = match palpite.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("Seu palpite: {}", palpite);

        match palpite.cmp(&numero_secreto) {
            Ordering::Less => println!("Muito baixo!"),
            Ordering::Greater => println!("Muito alto!"),
            Ordering::Equal => {
                println!("Você ganhou!");
                break;
            }
        }
    }
}

Listagem 2-5: Ignorando um palpite não numérico e pedindo outro palpite em vez de travar o programa.

Mudar de uma chamada expect para uma expressão match é como você geralmente muda de um erro para lidar com o erro. Lembre-se de que parse retorna um tipo Result e Result é um enum que possui as variantes Ok ou Err. Estamos usando uma expressão match aqui, como fizemos com o resultado Ordering do método cmp.

Se parse for capaz de transformar a string em um número com sucesso, ele retornará o valor Ok que contém o número resultante. Esse valor Ok corresponderá ao padrão da primeira ramificação e a expressão match apenas retornará o valor num que parse produziu e colocou dentro do valor Ok. Esse número vai acabar exatamente onde o queremos na nova variável palpite que estamos criando.

Se parse não for capaz de transformar a string em um número, ele retornará um valor que contém mais informações sobre o erro. O valor não corresponde ao padrão na primeira ramificação, mas corresponde ao padrão na segunda ramificação. O sublinhado, _, é um valor genérico; neste exemplo, estamos dizendo que queremos combinar todos os valores, não importa quais informações eles tenham dentro deles. Portanto, o programa executará o código da segunda ramificação, que diz ao programa para ir para a próxima iteração e solicitar outro palpite. Portanto, efetivamente, o programa ignora todos os erros que possa encontrar!

Agora, tudo no programa deve funcionar conforme o esperado. Vamos tentar:

>cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target\debug\guessing_game.exe`
Adivinhe o número!
O número secreto é: 17
Por favor entre com o seu palpite.
5
Seu palpite: 5
Muito baixo!
Por favor entre com o seu palpite.
56
Seu palpite: 56
Muito alto!
Por favor entre com o seu palpite.
sair
Por favor entre com o seu palpite.
17
Seu palpite: 17
Você ganhou!
    

Impressionante! Com um pequeno ajuste final, terminaremos o jogo de adivinhação. Lembre-se de que o programa ainda está imprimindo o número secreto. Funcionou bem para o teste, mas estragou o jogo. Vamos deletar o println! que mostra o número secreto. A Listagem 2-6 mostra o código final.

Nome do arquivo: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Adivinhe o número!");

    let numero_secreto = rand::thread_rng().gen_range(1, 101);

    loop {

        println!("Por favor entre com o seu palpite.");

        let mut palpite = String::new();

        io::stdin()
            .read_line(&mut palpite)
            .expect("Falha ao ler a linha.");

        let palpite: u32 = match palpite.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("Seu palpite: {}", palpite);

        match palpite.cmp(&numero_secreto) {
            Ordering::Less => println!("Muito baixo!"),
            Ordering::Greater => println!("Muito alto!"),
            Ordering::Equal => {
                println!("Você ganhou!");
                break;
            }
        }
    }
}

Listagem 2-6: Código completo do jogo de adivinhação

Resumo

Neste ponto, você construiu com sucesso o jogo de adivinhação. Parabéns!

Este projeto foi uma maneira "mãos à obra" de apresentá-lo a muitos conceitos novos do Rust: let, match, métodos, funções associadas, o uso de crates externos, e muito mais. Nos próximos capítulos, você aprenderá sobre esses conceitos com mais detalhes. O Capítulo 3 cobre os conceitos que a maioria das linguagens de programação tem, como variáveis, tipos de dados e funções, e mostra como usá-los no Rust. O Capítulo 4 explora a propriedade, um recurso que torna o Rust diferente de outras linguagens. O Capítulo 5 discute estruturas e sintaxe de método, e o Capítulo 6 explica como funcionam os enums.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença