Introdução
Bem-vindo(a) a Programando um Jogo de Adivinhação. Este laboratório faz parte do Livro de Rust. Você pode praticar suas habilidades em Rust no LabEx.
Neste laboratório, implementaremos um jogo de adivinhação em Rust, onde o programa gera um número aleatório e solicita ao jogador que o adivinhe, fornecendo feedback sobre se o palpite é muito baixo ou muito alto, e parabenizando o jogador se ele adivinhar corretamente.
Programando um Jogo de Adivinhação
Vamos mergulhar em 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, crates externas e muito mais! Nos capítulos seguintes, exploraremos essas ideias com mais detalhes. Neste capítulo, você apenas praticará os fundamentos.
Implementaremos um problema clássico de programação para iniciantes: um jogo de adivinhação. Veja como funciona: o programa gerará um inteiro aleatório entre 1 e 100. Em seguida, solicitará ao jogador que insira um palpite. Após a entrada de um palpite, o programa indicará se o palpite é muito baixo ou muito alto. Se o palpite estiver correto, o jogo imprimirá uma mensagem de parabéns e sairá.
Configurando um Novo Projeto
Para configurar um novo projeto, vá para o diretório project que você criou no Capítulo 1 e crie um novo projeto usando o Cargo, da seguinte forma:
cargo new guessing_game
cd guessing_game
O primeiro comando, cargo new, recebe o nome do projeto (guessing_game) como o primeiro argumento. O segundo comando muda para o diretório do novo projeto.
Observe o arquivo Cargo.toml gerado:
Nome do arquivo: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
## See more keys and their definitions at
https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Como você viu no Capítulo 1, cargo new gera um programa "Olá, mundo!" para você. Verifique o arquivo src/main.rs:
Nome do arquivo: src/main.rs
fn main() {
println!("Hello, world!");
}
Agora, vamos compilar este programa "Olá, mundo!" e executá-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 do jogo de adivinhação solicitará a entrada do usuário, processará essa entrada e verificará se ela está na forma esperada. Para começar, permitiremos que o jogador insira um palpite. Insira o código da Listagem 2-1 em src/main.rs.
Nome do arquivo: src/main.rs
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Listagem 2-1: Código que obtém um palpite do usuário e o imprime
Este código contém muitas informações, então vamos analisá-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 de entrada/saída para o escopo. A biblioteca io vem da biblioteca padrão, conhecida como std:
use std::io;
Por padrão, o Rust tem um conjunto de itens definidos na biblioteca padrão que ele traz para o escopo de cada programa. Este conjunto é chamado de prelude (preâmbulo), e você pode ver tudo nele em https://doc.rust-lang.org/std/prelude/index.html.
Se um tipo que você deseja usar não estiver no preâmbulo, você deve trazer esse tipo para o escopo explicitamente com uma declaração use. Usar a biblioteca std::io fornece uma série de recursos úteis, incluindo a capacidade de aceitar a entrada do usuário.
Como você viu no Capítulo 1, a função main é o ponto de entrada no 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!("Guess the number!");
println!("Please input your guess.");
Este código está imprimindo um prompt que informa qual é o jogo e solicitando a entrada do usuário.
Armazenando Valores com Variáveis
Em seguida, criaremos uma variável para armazenar a entrada do usuário, assim:
let mut guess = String::new();
Agora o programa está ficando interessante! Há muita coisa acontecendo nesta pequena linha. Usamos a declaração let para criar a variável. Aqui está outro exemplo:
let apples = 5;
Esta linha cria uma nova variável chamada apples e a vincula ao valor 5. Em Rust, as variáveis são imutáveis por padrão, o que significa que, uma vez que damos um valor à variável, o valor não mudará. Discutiremos este conceito em detalhes em "Variáveis e Mutabilidade". Para tornar uma variável mutável, adicionamos mut antes do nome da variável:
let apples = 5; // immutable
let mut bananas = 5; // mutable
Nota: A sintaxe
//inicia um comentário que continua até o final da linha. Rust ignora tudo nos comentários. Discutiremos os comentários com mais detalhes no Capítulo 3.
Voltando ao programa do jogo de adivinhação, você agora sabe que let mut guess introduzirá uma variável mutável chamada guess. O sinal de igual (=) diz ao Rust que queremos vincular algo à variável agora. À direita do sinal de igual está o valor ao qual guess está vinculado, 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 pedaço de texto UTF-8 codificado e expansível.
A sintaxe :: na linha ::new indica que new é uma função associada do tipo String. Uma função associada é uma função que é implementada em um tipo, neste caso String. 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.
Em suma, a linha let mut guess = String::new(); criou uma variável mutável que está atualmente vinculada a uma nova instância vazia de String. Ufa!
Recebendo a Entrada do Usuário
Lembre-se de que incluímos a funcionalidade de entrada/saída da biblioteca padrão com use std::io; na primeira linha do programa. Agora, chamaremos a função stdin do módulo io, o que nos permitirá lidar com a entrada do usuário:
io::stdin()
.read_line(&mut guess)
Se não tivéssemos importado a biblioteca io com use std::io; no início do programa, ainda poderíamos usar a função escrevendo esta 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 manipulador para a entrada padrão do seu terminal.
Em seguida, a linha .read_line(&mut guess) chama o método read_line no manipulador de entrada padrão para obter a entrada do usuário. Também estamos passando &mut guess como o argumento para read_line para dizer a ele em qual string armazenar a entrada do usuário. O trabalho completo de read_line é pegar o que o usuário digita na entrada padrão e anexá-lo a uma string (sem substituir seu conteúdo), então, portanto, passamos essa string como um argumento. O argumento da string precisa ser mutável para que o método possa alterar o conteúdo da string.
O & indica que este argumento é uma referência (reference), que oferece uma maneira de permitir que várias partes do seu código acessem um pedaço de dados sem precisar copiar esses dados na memória várias vezes. Referências são um recurso complexo, e uma das principais vantagens do Rust é como é seguro e fácil usar referências. Você não precisa saber muitos desses detalhes para concluir este programa. Por enquanto, tudo o que você precisa saber é que, como as variáveis, as referências são imutáveis por padrão. Portanto, você precisa escrever &mut guess em vez de &guess para torná-lo mutável. (O Capítulo 4 explicará as referências com mais detalhes.)
Lidando com Potenciais Falhas com Result
Ainda estamos trabalhando nesta linha de código. Agora estamos discutindo uma terceira linha de texto, mas observe que ela ainda faz parte de uma única linha lógica de código. A próxima parte é este método:
.expect("Failed to read line");
Poderíamos ter escrito este código como:
io::stdin().read_line(&mut guess).expect("Failed to read line");
No entanto, uma linha longa é difícil de ler, por isso é melhor dividi-la. Muitas vezes, é sensato introduzir uma nova linha e outros espaços em branco para ajudar a quebrar linhas longas quando você chama um método com a sintaxe .method_name(). Agora, vamos discutir o que esta linha faz.
Como mencionado anteriormente, read_line coloca o que o usuário insere na string que passamos para ela, mas também retorna um valor Result. Result é uma enumeração (enumeration), frequentemente chamada de enum, que é um tipo que pode estar em um de vários estados possíveis. Chamamos cada estado possível de variante (variant).
O Capítulo 6 cobrirá enums com mais detalhes. O objetivo desses tipos Result é codificar informações de tratamento de erros.
As variantes de Result são Ok e 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.
Valores do tipo Result, como valores de qualquer tipo, têm métodos definidos neles. Uma instância de Result tem um método expect que você pode chamar. Se esta instância de Result for um valor Err, expect fará com que o programa trave e exiba a mensagem que você passou como argumento para expect. Se o método read_line retornar um Err, provavelmente será o resultado de um erro vindo do sistema operacional subjacente. Se esta instância de Result for um valor Ok, expect pegará o valor de retorno que Ok está mantendo e retornará apenas esse valor para você, para que você possa usá-lo. Neste caso, esse valor é o número de bytes na entrada do usuário.
Se você não chamar expect, o programa compilará, mas você receberá um aviso:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust avisa que você não usou o valor Result retornado de read_line, indicando que o programa não tratou um possível erro.
A maneira correta de suprimir o aviso é realmente escrever código de tratamento de erros, mas, em nosso caso, só queremos que este programa trave quando um problema ocorrer, então podemos usar expect. Você aprenderá sobre como se recuperar de erros no Capítulo 9.
Imprimindo Valores com Placeholders println!
Além da chave de fechamento, há apenas mais uma linha para discutir no código até agora:
println!("You guessed: {guess}");
Esta linha imprime a string que agora contém a entrada do usuário. O conjunto de chaves {} é um placeholder (espaço reservado): pense em {} como pequenas pinças de caranguejo que seguram um valor no lugar. Ao imprimir o valor de uma variável, o nome da variável pode ir dentro das chaves. Ao imprimir o resultado da avaliação de uma expressão, coloque chaves vazias na string de formato e, em seguida, siga a string de formato com uma lista separada por vírgulas de expressões para imprimir em cada placeholder de chave vazio na mesma ordem. Imprimir uma variável e o resultado de uma expressão em uma chamada para println! ficaria assim:
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
Este código imprimiria x = 5 and y = 12.
Testando a Primeira Parte
Vamos testar a primeira parte do jogo de adivinhação. Execute-o usando cargo run:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
Neste ponto, a primeira parte do jogo está concluída: estamos recebendo a entrada do teclado e, em seguida, imprimindo-a.
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 o jogo seja divertido de jogar mais de uma vez. Usaremos um número aleatório entre 1 e 100 para que o jogo não seja muito difícil. O Rust ainda não inclui a funcionalidade de número aleatório em sua biblioteca padrão. No entanto, a equipe Rust fornece um crate rand em https://crates.io/crates/rand com essa funcionalidade.
Usando um Crate para Obter Mais Funcionalidade
Lembre-se que um crate é uma coleção de arquivos de código-fonte Rust. O projeto que estamos construindo é um crate binário, que é um executável. O crate rand é um crate de biblioteca, que contém código que se destina a ser usado em outros programas e não pode ser executado por conta própria.
A coordenação de crates externos pelo Cargo é onde o Cargo realmente se destaca. Antes de podermos escrever 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 no final, abaixo do cabeçalho da seção [dependencies] que o Cargo criou para você. Certifique-se de especificar rand exatamente como fizemos aqui, com este número de versão, ou os exemplos de código neste tutorial podem não funcionar:
Nome do arquivo: Cargo.toml
[dependencies]
rand = "0.8.5"
No arquivo Cargo.toml, tudo o que segue um cabeçalho faz parte dessa seção que continua até que outra seção comece. Em [dependencies] você informa ao Cargo quais crates externos seu projeto depende e quais versões desses crates você precisa. Neste caso, especificamos o crate rand com o especificador de versão semântica 0.8.5. O Cargo entende o Semantic Versioning (às vezes chamado de SemVer), que é um padrão para escrever números de versão. O especificador 0.8.5 é na verdade uma abreviação de ^0.8.5, o que significa qualquer versão que seja pelo menos 0.8.5, mas abaixo de 0.9.0.
O Cargo considera que essas versões têm APIs públicas compatíveis com a versão 0.8.5, e essa especificação garante que você obterá o último lançamento de patch que ainda compilará com o código neste capítulo. Qualquer versão 0.9.0 ou superior não tem garantia de ter a mesma API do que os exemplos a seguir usam.
Agora, sem alterar nenhum código, vamos construir o projeto, conforme mostrado na Listagem 2-2.
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Listagem 2-2: A saída da execução de cargo build após adicionar o crate rand como uma dependência
Você pode ver números de versão diferentes (mas todos serão compatíveis com o código, graças ao SemVer!) e linhas diferentes (dependendo do sistema operacional), e as linhas podem estar em uma ordem diferente.
Quando incluímos uma dependência externa, o Cargo busca as versões mais recentes de tudo o que essa dependência precisa do registry, que é uma cópia dos dados do Crates.io em https://crates.io. Crates.io é onde as pessoas no ecossistema Rust postam seus projetos Rust de código aberto para que outros usem.
Após atualizar o registro, o Cargo verifica a seção [dependencies] e baixa quaisquer crates listados que ainda não foram baixados. Neste caso, embora tenhamos listado apenas rand como uma dependência, o Cargo também pegou outros crates dos quais rand depende para funcionar. Após baixar os crates, o Rust os compila e, em seguida, 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 seu arquivo Cargo.toml. O Cargo também sabe que você não alterou nada sobre seu código, então ele também não recompila isso. Sem nada para fazer, ele simplesmente sai.
Se você abrir o arquivo src/main.rs, fizer uma alteração trivial e, em seguida, salvá-lo e construir novamente, você 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 só atualiza a compilação com sua pequena alteração no arquivo src/main.rs. Suas dependências não foram alteradas, então o Cargo sabe que pode reutilizar o que já baixou e compilou para elas.
Garantindo Builds Reproduzíveis com o Arquivo Cargo.lock
O Cargo possui um mecanismo que garante que você possa reconstruir o mesmo artefato toda vez que você ou qualquer outra pessoa construir seu código: o Cargo usará apenas as versões das dependências que você especificou até que você indique o contrário. Por exemplo, digamos que na próxima semana a versão 0.8.6 do crate rand seja lançada, e essa versão contenha uma correção de bug importante, mas também contenha uma regressão que irá quebrar seu código. Para lidar com isso, o Rust cria o arquivo Cargo.lock na primeira vez que você executa cargo build, então agora temos isso no diretório guessing_game.
Quando você constrói um projeto pela primeira vez, o Cargo descobre todas as versões das dependências que se encaixam nos critérios e, em seguida, as escreve no arquivo Cargo.lock. Quando você constrói seu projeto no futuro, o 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 build reproduzível automaticamente. Em outras palavras, seu projeto permanecerá na versão 0.8.5 até que você faça o upgrade explicitamente, graças ao arquivo Cargo.lock. Como o arquivo Cargo.lock é importante para builds reproduzíveis, ele geralmente é verificado no controle de origem com o restante do código em seu projeto.
Atualizando um Crate para Obter uma Nova Versão
Quando você quiser atualizar um crate, o Cargo fornece o comando update, que ignorará o arquivo Cargo.lock e descobrirá todas as versões mais recentes que se encaixam em suas especificações em Cargo.toml. O Cargo então escreverá essas versões no arquivo Cargo.lock. Caso contrário, por padrão, o Cargo só procurará versões maiores que 0.8.5 e menores que 0.9.0. Se o crate rand lançou as duas novas versões 0.8.6 e 0.9.0, você veria o seguinte se executasse cargo update:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 - > v0.8.6
O Cargo ignora o lançamento 0.9.0. 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.8.6. Para usar a versão 0.9.0 do rand ou qualquer versão da série 0.9._x_, você teria que atualizar o arquivo Cargo.toml para que se parecesse com isto:
[dependencies]
rand = "0.9.0"
Na próxima vez que você executar cargo build, o Cargo atualizará o registro de crates disponíveis e reavaliará seus requisitos de rand de acordo com a nova versão que você especificou.
Há muito mais a dizer sobre o Cargo e seu ecossistema, que discutiremos no Capítulo 14, mas por enquanto, isso é tudo o que você precisa saber. O Cargo torna muito fácil reutilizar bibliotecas, então os Rustaceans são capazes de escrever projetos menores que são montados a partir de vários pacotes.
Gerando um Número Aleatório
Vamos começar a usar rand para gerar um número para adivinhar. O próximo passo é atualizar src/main.rs, conforme mostrado na Listagem 2-3.
Nome do arquivo: src/main.rs
use std::io;
1 use rand::Rng;
fn main() {
println!("Guess the number!");
2 let secret_number = rand::thread_rng().gen_range(1..=100);
3 println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Listagem 2-3: Adicionando código para gerar um número aleatório
Primeiro, adicionamos a linha use rand::Rng; [1]. A trait Rng define métodos que os geradores de números aleatórios implementam, e essa trait deve estar no escopo para que possamos usar esses métodos. O Capítulo 10 cobrirá traits em detalhes.
Em seguida, estamos adicionando duas linhas no meio. Na primeira linha [2], chamamos a função rand::thread_rng que nos dá o gerador de números aleatórios específico que vamos usar: um que é local para o thread de execução atual e é semeado pelo sistema operacional. Então, chamamos o método gen_range no gerador de números aleatórios. Este método é definido pela trait Rng que trouxemos para o escopo com a declaração use rand::Rng;. O método gen_range recebe uma expressão de intervalo como argumento e gera um número aleatório no intervalo. O tipo de expressão de intervalo que estamos usando aqui assume a forma start..=end e é inclusivo nos limites inferior e superior, então precisamos especificar 1..=100 para solicitar um número entre 1 e 100.
Nota: Você não saberá apenas quais traits usar e quais métodos e funções chamar de um crate, então cada crate tem documentação com instruções para usá-lo. Outra característica interessante do Cargo é que a execução do comando
cargo doc --openconstruirá a documentação fornecida por todas as suas dependências localmente e a abrirá em seu navegador. Se você estiver interessado em outras funcionalidades no craterand, por exemplo, executecargo doc --opene clique emrandna barra lateral esquerda.
A segunda linha nova [3] 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 é muito um jogo se o programa imprimir a resposta assim que começar!
Tente executar o programa algumas vezes:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
Você deve obter números aleatórios diferentes, e todos eles 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 não compilará ainda, como explicaremos.
Nome do arquivo: src/main.rs
use rand::Rng;
1 use std::cmp::Ordering;
use std::io;
fn main() {
--snip--
println!("You guessed: {guess}");
2 match guess.3 cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Listagem 2-4: Lidando com os possíveis valores de retorno da comparação de dois números
Primeiro, adicionamos outra declaração use [1], trazendo um tipo chamado std::cmp::Ordering para o escopo da biblioteca padrão. O tipo Ordering é outro enum e tem as variantes Less, Greater e Equal. 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 [3] compara dois valores e pode ser chamado em qualquer coisa que possa ser comparada. Ele recebe uma referência ao que você deseja comparar: aqui, ele está comparando guess com secret_number. Em seguida, ele retorna uma variante do enum Ordering que trouxemos para o escopo com a declaração use. Usamos uma expressão match [2] para decidir o que fazer a seguir com base em qual variante de Ordering foi retornada da chamada para cmp com os valores em guess e secret_number.
Uma expressão match é composta por braços (arms). Um braço consiste em um padrão (pattern) para corresponder e o código que deve ser executado se o valor fornecido ao match corresponder ao padrão desse braço. Rust pega o valor fornecido ao match e percorre o padrão de cada braço por sua vez. Padrões e a construção match são recursos poderosos do Rust: eles permitem que você expresse uma variedade de situações que seu código pode encontrar e garantem que você as trate todas. Esses recursos serão abordados em detalhes no Capítulo 6 e no Capítulo 18, respectivamente.
Vamos analisar um exemplo com a expressão match que usamos aqui. Digamos que o usuário tenha adivinhado 50 e o número secreto gerado aleatoriamente desta vez seja 38.
Quando o código compara 50 a 38, o método cmp retornará Ordering::Greater porque 50 é maior que 38. A expressão match recebe o valor Ordering::Greater e começa a verificar o padrão de cada braço. Ele olha para o padrão do primeiro braço, Ordering::Less, e vê que o valor Ordering::Greater não corresponde a Ordering::Less, então ele ignora o código nesse braço e passa para o próximo braço. O padrão do próximo braço é Ordering::Greater, que corresponde a Ordering::Greater! O código associado nesse braço será executado e imprimirá Too big! na tela. A expressão match termina após a primeira correspondência bem-sucedida, portanto, não analisará o último braço nesse cenário.
No entanto, o código na Listagem 2-4 não compilará ainda. Vamos tentar:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `String`, found integer
|
= note: expected reference `&String`
found reference `&{integer}`
O cerne do erro afirma que há tipos incompatíveis. Rust tem um sistema de tipos forte e estático. No entanto, ele também tem inferência de tipo. Quando escrevemos let mut guess = String::new(), Rust foi capaz de inferir que guess deveria ser um String e não nos fez escrever o tipo. O secret_number, por outro lado, é um tipo numérico. Alguns dos tipos numéricos do Rust podem ter um valor entre 1 e 100: i32, um número de 32 bits; u32, um número sem sinal de 32 bits; i64, um número de 64 bits; bem como outros. A menos que especificado de outra forma, Rust assume i32, que é o tipo de secret_number, a menos que você adicione informações de tipo em outro lugar que fariam com que Rust inferisse um tipo numérico diferente. A razão do erro é que Rust não pode comparar uma string e um tipo numérico.
Em última análise, queremos converter o String que o programa lê como entrada em um tipo numérico real para que possamos compará-lo numericamente com o número secreto. Fazemos isso adicionando esta 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!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess
.trim()
.parse()
.expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Criamos uma variável chamada guess. Mas espere, o programa já não tem uma variável chamada guess? Tem, mas, felizmente, Rust nos permite sombrear o valor anterior de guess com um novo. Shadowing (sombreamento) nos permite reutilizar o nome da variável guess em vez de nos forçar a criar duas variáveis exclusivas, como guess_str e guess, por exemplo. Abordaremos isso com mais detalhes no Capítulo 3, mas, por enquanto, saiba que esse recurso é frequentemente usado quando você deseja converter um valor de um tipo para outro tipo.
Vinculamos essa nova variável à expressão guess.trim().parse(). O guess na expressão se refere à variável guess original que continha a entrada como uma string. O método trim em uma instância String eliminará qualquer espaço em branco no início e no final, o que devemos fazer para poder comparar a string com o u32, que só pode conter dados numéricos. O usuário deve pressionar Enter para satisfazer read_line e inserir seu palpite, o que adiciona um caractere de nova linha à string. Por exemplo, se o usuário digitar 5 e pressionar Enter, guess ficará assim: 5\n. O \n representa "nova linha". (No Windows, pressionar Enter resulta em um retorno de carro e uma nova linha, \r\n.) O método trim elimina \n ou \r\n, resultando em apenas 5.
O método parse em strings converte uma string em outro tipo. Aqui, usamos para converter de uma string para um número. Precisamos dizer ao Rust o tipo de número exato que queremos usando let guess: u32. Os dois pontos (:) após guess dizem ao Rust que vamos anotar o tipo da variável. Rust tem alguns tipos numéricos embutidos; 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 numéricos no Capítulo 3.
Além disso, a anotação u32 neste programa de exemplo e a comparação com secret_number significam que Rust inferirá que secret_number também deve ser um u32. Então, agora a comparação será entre dois valores do mesmo tipo!
O método parse só funcionará em caracteres que podem ser logicamente convertidos em números e, portanto, pode facilmente causar erros. Se, por exemplo, a string contivesse A👍%, não haveria como converter isso em um número. Como pode falhar, o método parse retorna um tipo Result, assim como o método read_line (discutido anteriormente em "Lidando com uma possível falha com Result"). Trataremos este Result da mesma forma, usando o método expect novamente. Se parse retornar um Err Result variante porque não conseguiu criar um número a partir da string, a chamada expect travará o jogo e imprimirá a mensagem que lhe damos. Se parse puder converter com sucesso a string em um número, ele retornará a variante Ok de Result, e expect retornará o número que queremos do valor Ok.
Vamos executar o programa agora:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
Legal! Mesmo que espaços tenham sido adicionados antes do palpite, 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 pode fazer apenas um palpite. Vamos mudar isso adicionando um loop!
Permitindo Vários Palpites com Loops
A palavra-chave loop cria um loop infinito. Adicionaremos um loop 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!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess
.trim()
.parse()
.expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
Como você pode ver, movemos tudo, desde o prompt de entrada do palpite em diante, para um loop. Certifique-se de indentar as linhas dentro do loop mais quatro espaços cada e execute o programa novamente. O programa agora pedirá outro palpite para sempre, o que na verdade introduz um novo problema. 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, como mencionado na discussão parse em "Comparando o Palpite com o Número Secreto": se o usuário inserir uma resposta que não seja um número, o programa travará. Podemos tirar proveito disso para permitir que o usuário saia, como mostrado aqui:
$ 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`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError
{ kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Digitar quit encerrará o jogo, mas, como você notará, também fará com que qualquer outra entrada que não seja um número seja encerrada. Isso é subótimo, para dizer o mínimo; queremos que o jogo também pare quando o número correto for adivinhado.
Saindo Após um Palpite Correto
Vamos programar o jogo para sair quando o usuário vencer, adicionando uma declaração break:
Nome do arquivo: src/main.rs
--snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
Adicionar a linha break após You win! faz com que o programa saia do loop quando o usuário adivinha o número secreto corretamente. Sair do loop também significa sair do programa, porque o loop é a última parte de main.
Lidando com Entrada Inválida
Para refinar ainda mais o comportamento do jogo, em vez de travar o programa quando o usuário insere algo que não é um número, vamos fazer com que o jogo ignore a entrada que não é um número para que o usuário possa continuar adivinhando. Podemos fazer isso alterando a linha onde guess é convertido de uma String para um u32, conforme mostrado na Listagem 2-5.
Nome do arquivo: src/main.rs
--snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
--snip--
Listagem 2-5: Ignorando um palpite que não é um número e pedindo outro palpite em vez de travar o programa
Mudamos de uma chamada expect para uma expressão match para passar de travar em um erro para lidar com o erro. Lembre-se que parse retorna um tipo Result e Result é um enum que tem as variantes Ok e Err. Estamos usando uma expressão match aqui, como fizemos com o resultado Ordering do método cmp.
Se parse conseguir transformar a string em um número, ele retornará um valor Ok que contém o número resultante. Esse valor Ok corresponderá ao padrão do primeiro braço, e a expressão match apenas retornará o valor num que parse produziu e colocou dentro do valor Ok. Esse número acabará exatamente onde queremos, na nova variável guess que estamos criando.
Se parse não conseguir transformar a string em um número, ele retornará um valor Err que contém mais informações sobre o erro. O valor Err não corresponde ao padrão Ok(num) no primeiro braço match, mas corresponde ao padrão Err(_) no segundo braço. O sublinhado, _, é um valor catchall; neste exemplo, estamos dizendo que queremos corresponder a todos os valores Err, não importa quais informações eles tenham dentro deles. Portanto, o programa executará o código do segundo braço, continue, que diz ao programa para ir para a próxima iteração do loop e pedir outro palpite. Então, efetivamente, o programa ignora todos os erros que parse pode encontrar!
Agora, tudo no programa deve funcionar como esperado. Vamos tentar:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
Incrível! Com um pequeno ajuste final, terminaremos o jogo de adivinhação. Lembre-se que o programa ainda está imprimindo o número secreto. Isso funcionou bem para testes, mas estraga 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 rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Listagem 2-6: Código completo do jogo de adivinhação
Neste ponto, você construiu com sucesso o jogo de adivinhação. Parabéns!
Resumo
Parabéns! Você concluiu o laboratório Programando um Jogo de Adivinhação. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.