Programmare un Gioco di Indovinelli
Immergiamoci in Rust lavorando insieme a un progetto pratico! Questo capitolo ti introdurrà a pochi concetti comuni di Rust mostrandoti come usarli in un programma reale. Imparerai a conoscere let
, match
, metodi, funzioni associate, crate esterne e altro ancora! Nei capitoli successivi, esploreremo queste idee in maggior dettaglio. In questo capitolo, eserciterai solo le basi.
Implementeremo un classico problema di programmazione per principianti: un gioco di indovinelli. Ecco come funziona: il programma genererà un intero casuale tra 1 e 100. Chiederà quindi al giocatore di inserire un indovinello. Dopo che un indovinello è stato inserito, il programma indicherà se l'indovinello è troppo basso o troppo alto. Se l'indovinello è corretto, il gioco stamperà un messaggio di congratulazioni ed uscirà.
Configurazione di un Nuovo Progetto
Per configurare un nuovo progetto, vai alla directory projects che hai creato nel Capitolo 1 e crea un nuovo progetto usando Cargo, come segue:
$ cargo new guessing_game
$ cd guessing_game
Il primo comando, cargo new
, prende il nome del progetto (guessing_game
) come primo argomento. Il secondo comando cambia nella directory del nuovo progetto.
Guarda il file Cargo.toml generato:
Nome del file: 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]
Come hai visto nel Capitolo 1, cargo new
genera un programma “Hello, world!” per te. Controlla il file src/main.rs:
Nome del file: src/main.rs
fn main() { println!("Hello, world!"); }
Ora compiliamo questo programma “Hello, world!” ed eseguiamolo nello stesso passaggio usando il 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!
Il comando run
è utile quando hai bisogno di iterare rapidamente su un progetto, come faremo in questo gioco, testando rapidamente ogni iterazione prima di passare alla successiva.
Riapri il file src/main.rs. Scriverai tutto il codice in questo file.
Elaborare un Indovinello
La prima parte del programma del gioco di indovinelli chiederà l'input dell'utente, elaborerà quell'input e verificherà che l'input sia nella forma prevista. Per iniziare, permetteremo al giocatore di inserire un indovinello. Inserisci il codice nell'Elenco 2-1 in 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}");
}
Questo codice contiene molte informazioni, quindi esaminiamolo riga per riga. Per ottenere l'input dell'utente e poi stampare il risultato come output, dobbiamo portare la libreria io
di input/output nello Scope. La libreria io
proviene dalla libreria standard, conosciuta come std
:
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}");
}
Per default, Rust ha un insieme di elementi definiti nella libreria standard che porta nello Scope di ogni programma. Questo insieme è chiamato prelude, e puoi vedere tutto ciò che contiene nella documentazione della libreria standard.
Se un tipo che vuoi usare non è nel prelude, devi portare esplicitamente quel tipo nello Scope con un'istruzione use
. Usare la libreria std::io
ti fornisce una serie di funzionalità utili, inclusa la possibilità di accettare input dall'utente.
Come hai visto nel Capitolo 1, la funzione main
è il punto di ingresso nel programma:
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}");
}
La sintassi fn
dichiara una nuova funzione; le parentesi tonde, ()
, indicano che non ci sono parametri; e la parentesi graffa, {
, inizia il Blocco della funzione.
Come hai anche imparato nel Capitolo 1, println!
è una macro che stampa una stringa a schermo:
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}");
}
Questo codice sta stampando un prompt che dichiara cosa sia il gioco e richiede un input dall'utente.
Memorizzare Valori con le Variabili
Successivamente, creeremo una variabile per memorizzare l'input dell'utente, come segue:
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}");
}
Ora il programma diventa interessante! C'è molto in corso in questa piccola riga. Usiamo l'istruzione let
per creare la variabile. Ecco un altro esempio:
let apples = 5;
Questa linea crea una nuova variabile chiamata apples
e la associa al valore 5. In Rust, le variabili sono immutabili per default, il che significa che una volta assegnato un valore alla variabile, questo valore non cambierà. Discuteremo questo concetto in dettaglio nella sezione “Variabili e Mutabilità” nel Capitolo 3. Per rendere una variabile mutabile, aggiungiamo mut
prima del nome della variabile:
let apples = 5; // immutabile
let mut bananas = 5; // mutabile
Nota: La sintassi
//
inizia un commento che continua fino alla fine della linea. Rust ignora tutto nei commenti. Discuteremo i commenti in modo più dettagliato nel Capitolo 3.
Tornando al programma del gioco di indovinelli, ora sai che let mut guess
introdurrà una variabile mutabile chiamata guess
. Il segno di uguale (=
) dice a Rust che vogliamo associare qualcosa alla variabile ora. A destra del segno di uguale c'è il valore a cui guess
è associato, ovvero il risultato della chiamata a String::new
, una funzione che restituisce una nuova istanza di una String
.
String
è un tipo di stringa fornito dalla libreria standard che è un testo UTF-8 codificato espandibile.
La sintassi ::
nella linea ::new
indica che new
è una funzione associata del tipo String
. Una funzione associata è una funzione che è implementata su un tipo, in questo caso String
. Questa funzione new
crea una nuova stringa vuota. Troverai una funzione new
su molti tipi perché è un nome comune per una funzione che crea un nuovo valore di qualche tipo.
In sintesi, la linea let mut guess = String::new();
ha creato una variabile mutabile che è attualmente associata a una nuova istanza vuota di una String
. Uff!
Ricevere Input dall'Utente
Ricordati che abbiamo incluso la funzionalità di input/output dalla libreria standard con use std::io;
nella prima linea del programma. Ora chiameremo la funzione stdin
dal modulo io
, il quale ci permetterà di gestire l'input dell'utente:
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}");
}
Se non avessimo importato la libreria io
con use std::io;
all'inizio del programma, potremmo ancora usare la funzione scrivendo questa chiamata di funzione come std::io::stdin
. La funzione stdin
restituisce un'istanza di std::io::Stdin
, che è un tipo che rappresenta un handle per l'input standard del tuo terminale.
Successivamente, la riga .read_line(&mut guess)
chiama il metodo read_line
sull'handle di input standard per ottenere l'input dall'utente. Passiamo anche &mut guess
come argomento a read_line
per dirgli in quale stringa memorizzare l'input dell'utente. Il lavoro completo di read_line
è prendere qualsiasi cosa l'utente digiti nell'input standard e appenderla a una stringa (senza sovrascrivere il suo contenuto), quindi passiamo quella stringa come argomento. La stringa ha bisogno di essere mutabile affinché il metodo possa cambiare il contenuto della stringa.
Il &
indica che questo argomento è una referenza, che ti dà un modo di far accedere parti multiple del tuo codice a un pezzo di dati senza doverne copiare i dati in memoria più volte. Le referenze sono una caratteristica complessa e uno dei maggiori vantaggi di Rust è quanto sia sicuro e facile usare le referenze. Non hai bisogno di sapere molti di quei dettagli per completare questo programma. Per ora, tutto ciò che devi sapere è che, come le variabili, le referenze sono immutabili per default. Pertanto, devi scrivere &mut guess
piuttosto che &guess
per renderla mutabile. (Il Capitolo 4 spiegherà le referenze in modo più approfondito.)
Gestire il Potenziale Fallimento con Result
Stiamo ancora lavorando su questa riga di codice. Ora stiamo discutendo una terza riga di testo, ma nota che fa ancora parte di una singola riga logica di codice. La parte successiva di questo metodo è:
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}");
}
Avremmo potuto scrivere questo codice come:
io::stdin().read_line(&mut guess).expect("Failed to read line");
Tuttavia, una riga lunga è difficile da leggere, quindi è meglio dividerla. È spesso saggio introdurre un nuovo riga e altri spazi bianchi per aiutare a spezzare righe lunghe quando chiami un metodo con la sintassi .method_name()
. Ora discutiamo cosa fa questa linea.
Come menzionato prima, read_line
mette qualsiasi cosa l'utente inserisca nella stringa che passiamo ad essa, ma restituisce anche un valore Result
. Result
è un enumeration, spesso chiamato enum, che è un tipo che può essere in uno di molti stati possibili. Chiamiamo ciascun stato possibile una variante.
Il Capitolo 6 coprirà gli enum in modo più dettagliato. Lo scopo di questi tipi Result
è codificare le informazioni sulla gestione degli errori.
Le varianti di Result
sono Ok
ed Err
. La variante Ok
indica che l'operazione è stata eseguita con successo, e dentro Ok
c'è il valore generato con successo. La variante Err
significa che l'operazione è fallita, e Err
contiene informazioni su come o perché l'operazione è fallita.
Valori del tipo Result
, come valori di qualsiasi tipo, hanno metodi definiti su di loro. Un'istanza di Result
ha un metodo expect
che puoi chiamare. Se questa istanza di Result
è un valore Err
, expect
farà sì che il programma si arresti e visualizzi il messaggio che hai passato come argomento a expect
. Se il metodo read_line
restituisce un Err
, è probabile che sia il risultato di un errore proveniente dal sistema operativo sottostante. Se questa istanza di Result
è un valore Ok
, expect
prenderà il valore di ritorno che Ok
sta trattenendo e restituirà solo quel valore a te in modo che tu possa usarlo. In questo caso, quel valore è il numero di byte nell'input dell'utente.
Se non chiami expect
, il programma si compila, ma otterrai un avvertimento:
$ 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: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust avverte che non hai utilizzato il valore Result
restituito da read_line
, indicando che il programma non ha gestito un possibile errore.
Il modo giusto per eliminare l'avviso è scrivere effettivamente codice di gestione degli errori, ma nel nostro caso vogliamo solo che questo programma si arresti quando si verifica un problema, quindi possiamo usare expect
. Imparerai a recuperare da errori nel Capitolo 9.
Stampare Valori con i Segnaposto di println!
A parte la parentesi graffa di chiusura, c'è solo un'altra riga di cui discutere nel codice finora:
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}");
}
Questa riga stampa la stringa che ora contiene l'input dell'utente. Il set di parentesi graffe {}
è un segnaposto: pensa a {}
come piccole chele che tengono un valore in posizione. Quando si stampa il valore di una variabile, il nome della variabile può andare dentro le parentesi graffe. Quando si stampa il risultato di una espressione, posiziona parentesi graffe vuote nella stringa di formato, quindi segui la stringa di formato con un elenco separato da virgole di espressioni da stampare in ogni segnaposto di parentesi graffe vuoto nello stesso ordine. Stampare una variabile e il risultato di un'espressione in una chiamata a println!
apparirebbe così:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
Questo codice stamperebbe x = 5 and y + 2 = 12
.
Testare la Prima Parte
Testiamo la prima parte del gioco di indovinelli. Eseguilo 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
A questo punto, la prima parte del gioco è finita: stiamo ottenendo input dalla tastiera e poi lo stampiamo.
Generare un Numero Segreto
Successivamente, dobbiamo generare un numero segreto che l'utente tenterà di indovinare. Il numero segreto dovrebbe essere diverso ogni volta affinché il gioco sia divertente da giocare più di una volta. Useremo un numero casuale tra 1 e 100 in modo che il gioco non sia troppo difficile. Rust non include ancora funzionalità di numeri casuali nella sua libreria standard. Tuttavia, il team di Rust fornisce un crate rand
con tale funzionalità.
Usare un Crate per Ottenere Maggiori Funzionalità
Ricorda che un crate è una raccolta di file di codice sorgente Rust. Il progetto su cui abbiamo lavorato è un crate binario, che è un eseguibile. Il crate rand
è un crate libreria, che contiene codice destinato a essere utilizzato in altri programmi e non può essere eseguito da solo.
Il coordinamento dei crate esterni da parte di Cargo è dove Cargo brilla davvero. Prima di poter scrivere codice che utilizza rand
, dobbiamo modificare il file Cargo.toml per includere il crate rand
come dipendenza. Apri quel file ora e aggiungi la seguente riga in fondo, sotto l'intestazione della sezione [dependencies]
che Cargo ha creato per te. Assicurati di specificare rand
esattamente come abbiamo fatto qui, con questo numero di versione, altrimenti i codici di esempio in questo tutorial potrebbero non funzionare:
Nome del file: Cargo.toml
[dependencies]
rand = "0.8.5"
Nel file Cargo.toml, tutto ciò che segue un'intestazione fa parte di quella sezione che continua fino a quando non inizia un'altra sezione. In [dependencies]
indichi a Cargo da quali crate esterni il tuo progetto dipende e quali versioni di quei crate richiedi. In questo caso, specifichiamo il crate rand
con lo specificatore di versione semantica 0.8.5
. Cargo comprende il Versionamento Semantico (a volte chiamato SemVer), che è uno standard per scrivere numeri di versione. Lo specificatore 0.8.5
è in realtà un'abbreviazione per ^0.8.5
, il che significa qualsiasi versione che sia almeno 0.8.5 ma inferiore a 0.9.0.
Cargo considera queste versioni come aventi API pubbliche compatibili con la versione 0.8.5, e questa specifica garantisce che otterrai l'ultima release di patch che continuerà a compilare con il codice in questo capitolo. Qualsiasi versione 0.9.0 o successiva non è garantita per avere la stessa API di quanto utilizzato nei seguenti esempi.
Ora, senza cambiare nessuno del codice, costruiamo il progetto, come mostrato nel Listing 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 libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
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
Potresti vedere numeri di versione diversi (ma saranno tutti compatibili con il codice, grazie a SemVer!) e linee diverse (a seconda del sistema operativo), e le linee potrebbero essere in un ordine diverso.
Quando includiamo una dipendenza esterna, Cargo recupera le ultime versioni di tutto ciò che quella dipendenza necessita dal registro, che è una copia dei dati da Crates.io. Crates.io è dove le persone nell'ecosistema Rust pubblicano i loro progetti Rust open source affinché altri possano utilizzarli.
Dopo aver aggiornato il registro, Cargo controlla la sezione [dependencies]
e scarica qualsiasi crate elencato che non è già stato scaricato. In questo caso, sebbene abbiamo elencato solo rand
come dipendenza, Cargo ha preso anche altri crate da cui rand
dipende per funzionare. Dopo aver scaricato i crate, Rust li compila e poi compila il progetto con le dipendenze disponibili.
Se esegui immediatamente cargo build
di nuovo senza apportare modifiche, non otterrai alcun output a parte la linea Finished
. Cargo sa già di aver scaricato e compilato le dipendenze e tu non hai cambiato nulla su di esse nel tuo file Cargo.toml. Cargo sa anche che non hai cambiato nulla sul tuo codice, quindi non lo ricompila nemmeno. Con nulla da fare, semplicemente si chiude.
Se apri il file src/main.rs, fai una modifica banale, poi lo salvi e lo compili di nuovo, vedrai solo due righe di output:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
Queste righe mostrano che Cargo aggiorna solo la build con la tua piccola modifica al file src/main.rs. Le tue dipendenze non sono cambiate, quindi Cargo sa che può riutilizzare ciò che ha già scaricato e compilato per quelle.
Garantire Build Riproducibili con il File Cargo.lock
Cargo ha un meccanismo che garantisce che tu possa ricostruire lo stesso artefatto ogni volta che tu o chiunque altro costruisce il tuo codice: Cargo utilizzerà solo le versioni delle dipendenze che hai specificato finché non indichi altrimenti. Ad esempio, supponiamo che la prossima settimana venga rilasciata la versione 0.8.6 del crate rand
e che quella versione contenga una correzione di bug importante, ma contenga anche una regressione che romperà il tuo codice. Per gestire ciò, Rust crea il file Cargo.lock la prima volta che esegui cargo build
, quindi ora abbiamo questo nella directory guessing_game.
Quando costruisci un progetto per la prima volta, Cargo determina tutte le versioni delle dipendenze che soddisfano i criteri e poi le scrive nel file Cargo.lock. Quando costruisci il tuo progetto in futuro, Cargo vedrà che il file Cargo.lock esiste e utilizzerà le versioni specificate lì piuttosto che fare tutto il lavoro di determinare di nuovo le versioni. Questo ti consente di avere una build riproducibile automaticamente. In altre parole, il tuo progetto rimarrà a 0.8.5 finché non effettui un aggiornamento esplicito, grazie al file Cargo.lock. Poiché il file Cargo.lock è importante per le build riproducibili, spesso viene inserito nel controllo del codice sorgente insieme al resto del codice nel tuo progetto.
Aggiornare un Crate per Ottenere una Nuova Versione
Quando vuoi veramente aggiornare un crate, Cargo fornisce il comando update
, che ignorerà il file Cargo.lock e determinerà tutte le ultime versioni che soddisfano le tue specifiche in Cargo.toml. Cargo scriverà poi quelle versioni nel file Cargo.lock. In questo caso, Cargo cercherà solo versioni maggiori di 0.8.5 e minori di 0.9.0. Se il crate rand
ha rilasciato le due nuove versioni 0.8.6 e 0.9.0, vedresti quanto segue se eseguissi cargo update
:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo ignora il rilascio della 0.9.0. A questo punto, noteresti anche un cambiamento nel tuo file Cargo.lock che indica che la versione del crate rand
che stai usando ora è 0.8.6. Per usare la versione 0.9.0 di rand
o qualsiasi versione nella serie 0.9.x, dovresti aggiornare il file Cargo.toml in modo che assomigli a questo:
[dependencies]
rand = "0.9.0"
La prossima volta che eseguirai cargo build
, Cargo aggiornerà il registro dei crate disponibili e rivaluterà le tue richieste rand
secondo la nuova versione che hai specificato.
Ci sarebbe molto altro da dire su Cargo e il suo ecosistema, di cui discuteremo nel Capitolo 14, ma per ora, questo è tutto ciò che devi sapere. Cargo rende molto facile riutilizzare le librerie, quindi i Rustaceans sono in grado di scrivere progetti più piccoli che sono assemblati da diversi pacchetti.
Generazione di un Numero Casuale
Iniziamo a utilizzare rand
per generare un numero da indovinare. Il passo successivo è aggiornare src/main.rs, come mostrato nel Listing 2-3.
use std::io;
use rand::Rng;
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");
println!("You guessed: {guess}");
}
Per prima cosa aggiungiamo la linea use rand::Rng;
. Il Rng
trait definisce metodi che i generatori di numeri casuali implementano, e questo trait deve essere nello Scope affinché possiamo utilizzare quei metodi. Il Capitolo 10 tratterà i trait in dettaglio.
Successivamente, stiamo aggiungendo due righe nel mezzo. Nella prima riga, chiamiamo la funzione rand::thread_rng
che ci fornisce il particolare generatore di numeri casuali che stiamo per utilizzare: uno che è locale al thread di esecuzione corrente ed è seminato dal sistema operativo. Poi chiamiamo il metodo gen_range
sul generatore di numeri casuali. Questo metodo è definito dal trait Rng
che abbiamo portato nello Scope con l'istruzione use rand::Rng;
. Il metodo gen_range
prende un'espressione di intervallo come argomento e genera un numero casuale nell'intervallo. Il tipo di espressione di intervallo che stiamo utilizzando qui ha la forma start..=end
ed è inclusivo sui limiti inferiore e superiore, quindi dobbiamo specificare 1..=100
per richiedere un numero tra 1 e 100.
Nota: Non saprai semplicemente quali trait usare e quali metodi e funzioni chiamare da un crate, quindi ogni crate ha documentazione con istruzioni per utilizzarlo. Un'altra caratteristica interessante di Cargo è che eseguire il comando
cargo doc --open
costruirà la documentazione fornita da tutte le tue dipendenze localmente e la aprirà nel tuo browser. Se sei interessato ad altre funzionalità nel craterand
, ad esempio, eseguicargo doc --open
e clicca surand
nella barra laterale a sinistra.
La seconda nuova linea stampa il numero segreto. Questo è utile mentre stiamo sviluppando il programma per poterlo testare, ma lo elimineremo dalla versione finale. Non è molto un gioco se il programma stampa la risposta appena inizia!
Prova a eseguire il programma alcune volte:
$ 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
Dovresti ottenere numeri casuali diversi, e dovrebbero essere tutti numeri tra 1 e 100. Ottimo lavoro!
Confrontare l'Indovinello con il Numero Segreto
Ora che abbiamo l'input dell'utente e un numero casuale, possiamo confrontarli. Quel passaggio è mostrato nel Listing 2-4. Nota che questo codice non sarà ancora compilabile, come spiegheremo.
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
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");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Per prima cosa aggiungiamo un'altra istruzione use
, portando un tipo chiamato std::cmp::Ordering
nello Scope dalla libreria standard. Il tipo Ordering
è un altro enum e ha le varianti Less
, Greater
, e Equal
. Questi sono i tre risultati possibili quando confronti due valori.
Poi aggiungiamo cinque nuove righe in basso che utilizzano il tipo Ordering
. Il metodo cmp
confronta due valori e può essere chiamato su qualsiasi cosa che possa essere confrontata. Prende un riferimento a qualsiasi cosa vuoi confrontare: qui sta confrontando guess
con secret_number
. Poi restituisce una variante dell'enum Ordering
che abbiamo portato nello Scope con l'istruzione use
. Utilizziamo un'espressione match
per decidere cosa fare successivamente in base a quale variante di Ordering
è stata restituita dalla chiamata a cmp
con i valori in guess
e secret_number
.
Un'espressione match
è costituita da Rami. Un Ramo consiste in un Pattern da abbinare e il codice che dovrebbe essere eseguito se il valore dato a match
si adatta a quel Pattern del Ramo. Rust prende il valore dato a match
e controlla ciascun Pattern del Ramo a turno. I Pattern e il costrutto match
sono potenti caratteristiche di Rust: ti permettono di esprimere una varietà di situazioni che il tuo codice potrebbe incontrare e ti assicurano di gestirle tutte. Queste caratteristiche saranno trattate in dettaglio nel Capitolo 6 e nel Capitolo 18, rispettivamente.
Vediamo un esempio con l'espressione match
che utilizziamo qui. Supponiamo che l'utente abbia indovinato 50 e che il numero segreto generato casualmente questa volta sia 38.
Quando il codice confronta 50 con 38, il metodo cmp
restituirà Ordering::Greater
perché 50 è maggiore di 38. L'espressione match
ottiene il valore Ordering::Greater
e inizia a verificare ogni Pattern del Ramo. Guarda il Pattern del primo Ramo, Ordering::Less
, e vede che il valore Ordering::Greater
non corrisponde a Ordering::Less
, quindi ignora il codice in quel Ramo e passa al prossimo Ramo. Il Pattern del prossimo Ramo è Ordering::Greater
, che corrisponde a Ordering::Greater
! Il codice associato in quel Ramo sarà eseguito e stamperà Too big!
sullo schermo. L'espressione match
termina dopo la prima corrispondenza riuscita, quindi non esaminerà l'ultimo Ramo in questo scenario.
Tuttavia, il codice nel Listing 2-4 non sarà ancora compilabile. Proviamo:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
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
| |
| arguments to this function are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: associated function defined here
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/cmp.rs:783:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` due to previous error
Il nucleo dell'errore afferma che ci sono tipi non corrispondenti. Rust ha un sistema tipologico forte e statico. Tuttavia, ha anche la determinazione del tipo. Quando abbiamo scritto let mut guess = String::new()
, Rust è stato in grado di inferire che guess
dovrebbe essere un String
e non ci ha costretti a scrivere il tipo. D'altra parte, secret_number
è un tipo numerico. Alcuni tipi numerici di Rust possono avere un valore tra 1 e 100: i32
, un numero a 32 bit; u32
, un numero Unsigned a 32 bit; i64
, un numero a 64 bit; oltre ad altri. A meno che non sia specificato diversamente, Rust predefinisce un i32
, che è il tipo di secret_number
a meno che tu non aggiunga informazioni di tipo altrove che farebbero inferire a Rust un tipo numerico diverso. La ragione dell'errore è che Rust non può confrontare una stringa e un tipo numerico.
Alla fine, vogliamo convertire il String
che il programma legge come input in un tipo numerico in modo da poterlo confrontare numericamente con il numero segreto. Lo facciamo aggiungendo questa riga al Blocco della funzione main
:
Nome file: 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);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
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!"),
}
}
La linea è:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
Creiamo una variabile chiamata guess
. Ma aspetta, il programma ha già una variabile chiamata guess
? Sì, ma Rust ci permette utilmente di sovrascrivere il valore precedente di guess
con uno nuovo. Shadowing ci permette di riutilizzare il nome della variabile guess
piuttosto che costringerci a creare due variabili uniche, come guess_str
e guess
, per esempio. Tratteremo questo in maggior dettaglio nel Capitolo 3, ma per ora, sappi che questa caratteristica è spesso utilizzata quando si desidera convertire un valore da un tipo a un altro tipo.
Noi leghiamo questa nuova variabile all'espressione guess.trim().parse()
. Il guess
nell'espressione si riferisce alla variabile guess
originale che conteneva
l'input come stringa. Il metodo trim
su un'istanza di String
eliminerà qualsiasi
spazio bianco all'inizio e alla fine, cosa che dobbiamo fare per poter confrontare la
stringa con il u32
, che può contenere solo dati numerici. L'utente deve premere
invio per soddisfare read_line
e inserire il loro guess, che aggiunge un
carattere di nuova linea alla stringa. Ad esempio, se l'utente digita 5 e
preme invio, guess
appare così: 5\n
. Il \n
rappresenta
“newline.” (Su Windows, premendo invio risulta in un ritorno a capo
e un newline, \r\n
.) Il metodo trim
elimina \n
o \r\n
, risultando
in solo 5
.
Il metodo parse
sulle stringhe converte una stringa in
un altro tipo. Qui, lo usiamo per convertire da una stringa a un numero. Dobbiamo
dire a Rust esattamente il tipo di numero che vogliamo usando let guess: u32
. Il duepunti
(:
) dopo guess
dice a Rust che annoteremo il tipo della variabile. Rust ha alcuni
tipi di numeri integrati; il u32
visto qui è un intero senza segno a 32-bit.
È una buona scelta predefinita per un piccolo numero positivo. Imparerai a conoscere
altri tipi di numeri nel Capitolo 3.
Inoltre, l'annotazione u32
in questo programma di esempio e la comparazione
con secret_number
significano che Rust dedurrà che secret_number
dovrebbe essere
anche un u32
. Quindi ora la comparazione sarà tra due valori dello stesso tipo!
Il metodo parse
funzionerà solo su caratteri che possono logicamente essere convertiti
in numeri e quindi può facilmente causare errori. Se, ad esempio, la stringa
conteneva A👍%
, non ci sarebbe modo di convertirlo in un numero. Poiché potrebbe
fallire, il metodo parse
restituisce un tipo Result
, proprio come il metodo
read_line
(discusso in precedenza in “Gestire i fallimenti potenziali con
Result
”). Tratteremo
questo Result
nello stesso modo usando di nuovo il metodo expect
. Se parse
restituisce una variante Err
Result
perché non è stato in grado di creare un numero dalla
stringa, la chiamata expect
farà crashare il gioco e stampare il messaggio che gli diamo.
Se parse
può convertire con successo la stringa in un numero, restituirà la
variante Ok
di Result
, e expect
restituirà il numero che vogliamo dal
valore Ok
.
Eseguiamo il programma ora:
$ 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`
Indovina il numero!
Il numero segreto è: 58
Per favore inserisci il tuo guess.
76
Hai indovinato: 76
Troppo grande!
Bello! Anche se erano stati aggiunti spazi prima del guess, il programma ha comunque capito che l'utente ha indovinato 76. Esegui il programma alcune volte per verificare il comportamento diverso con diversi tipi di input: indovina il numero correttamente, indovina un numero che è troppo alto, e indovina un numero che è troppo basso.
Abbiamo quasi tutto il gioco funzionante ora, ma l'utente può fare solo un guess. Modifichiamolo aggiungendo un loop!
Permettere Molteplici Guess con Looping
La keyword loop
crea un loop infinito. Aggiungeremo un loop per dare agli utenti
più possibilità di indovinare il numero:
Filename: 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);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
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!"),
}
}
}
Come puoi vedere, abbiamo spostato tutto dal prompt di inserimento del guess in poi dentro un loop. Assicurati di indentare le righe all'interno del loop di ulteriori quattro spazi ciascuna e esegui nuovamente il programma. Ora il programma chiederà un altro guess all'infinito, il che introduce un nuovo problema. Non sembra che l'utente possa uscire!
L'utente potrebbe sempre interrompere il programma utilizzando la scorciatoia da tastiera
ctrl-c. Ma c'è un altro modo per sfuggire a questo insaziabile
mostro, come menzionato nella discussione del parse
in “Comparare il Guess al
Numero Segreto”: se
l'utente inserisce una risposta non numerica, il programma si bloccherà. Possiamo trarre
vantaggio di ciò per consentire all'utente di uscire, come mostrato qui:
$ 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`
Indovina il numero!
Il numero segreto è: 59
Per favore inserisci il tuo guess.
45
Hai indovinato: 45
Troppo piccolo!
Per favore inserisci il tuo guess.
60
Hai indovinato: 60
Troppo grande!
Per favore inserisci il tuo guess.
59
Hai indovinato: 59
Hai vinto!
Per favore inserisci il tuo guess.
quit
thread 'main' panicked at 'Per favore digita un numero!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Digitare quit
uscirà dal gioco, ma come noterai, anche inserire qualsiasi
altro input non numerico farà lo stesso. Questo è subottimale, per non dire altro; vogliamo che il gioco
si fermi anche quando il numero corretto è stato indovinato.
Uscire Dopo un Indovinamento Corretto
Programmiamo il gioco per uscire quando l'utente vince aggiungendo una dichiarazione break
:
Filename: 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);
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}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Aggiungere la riga break
dopo Hai vinto!
fa uscire il programma dal loop
quando l'utente indovina correttamente il numero segreto. Uscire dal loop significa
anche uscire dal programma, perché il loop è l'ultima parte di main
.
Gestire Input Non Validi
Per affinare ulteriormente il comportamento del gioco, invece di far crashare il programma quando
l'utente inserisce un non-numero, facciamo in modo che il gioco ignori un non-numero così l'utente
può continuare a indovinare. Possiamo farlo modificando la riga in cui guess
è convertito da una String
a un u32
, come mostrato nel Listing 2-5.
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);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --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--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Passiamo da una chiamata expect
a un'espressione match
per passare dal crash
su un errore alla gestione dell'errore. Ricorda che parse
restituisce un tipo
Result
e Result
è un enum che ha le varianti Ok
e Err
. Stiamo usando
un'espressione match
qui, come abbiamo fatto con il risultato di Ordering
del metodo cmp
.
Se parse
è in grado di convertire con successo la stringa in un numero, restituirà
un valore Ok
che contiene il numero risultante. Quel valore Ok
corrisponderà
al pattern del primo Ramo, e l'espressione match
restituirà semplicemente il valore
num
che parse
ha prodotto e inserito nel valore Ok
. Quel numero
finirà esattamente dove lo vogliamo nella nuova variabile guess
che stiamo creando.
Se parse
non è in grado di trasformare la stringa in un numero, restituirà un
valore Err
che contiene ulteriori informazioni sull'errore. Il valore Err
non corrisponde al pattern Ok(num)
nel primo Ramo match
, ma corrisponde
al pattern Err(_)
nel secondo Ramo. Il trattino basso, _
, è un
valore di ripiego; in questo esempio, stiamo dicendo che vogliamo far corrispondere tutti i valori
Err
, non importa quali informazioni abbiano al loro interno. Quindi il programma
eseguirà il codice del secondo Ramo, continue
, che dice al programma di passare
all'iterazione successiva del loop
e chiedere un altro guess. Quindi, effettivamente,
il programma ignorerà tutti gli errori che parse
potrebbe incontrare!
Ora tutto nel programma dovrebbe funzionare come previsto. Proviamolo:
$ 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`
Indovina il numero!
Il numero segreto è: 61
Per favore inserisci il tuo guess.
10
Hai indovinato: 10
Troppo piccolo!
Per favore inserisci il tuo guess.
99
Hai indovinato: 99
Troppo grande!
Per favore inserisci il tuo guess.
foo
Per favore inserisci il tuo guess.
61
Hai indovinato: 61
Hai vinto!
Fantastico! Con un piccolo ultimo aggiustamento, finiremo il gioco dell'indovinello. Ricorda
che il programma sta ancora stampando il numero segreto. È stato utile per
testare, ma rovina il gioco. Eliminiamo il println!
che stampa il
numero segreto. Il Listing 2-6 mostra il codice finale.
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;
}
}
}
}
A questo punto, hai costruito con successo il gioco dell'indovinello. Congratulazioni!
Sommario
Questo progetto è stato un modo pratico per introdurti a molti nuovi concetti di Rust:
let
, match
, funzioni, l'uso di crate esterni, e altro. Nei prossimi
capitoli, imparerai questi concetti in maggiore dettaglio. Il Capitolo 3
copre concetti che la maggior parte dei linguaggi di programmazione hanno, come variabili, tipi
di dati e funzioni, e mostra come usarli in Rust. Il Capitolo 4 esplora
ownership, una caratteristica che rende Rust diverso dagli altri linguaggi. Il Capitolo 5
discute structs e sintassi dei metodi, e il Capitolo 6 spiega come funzionano gli enum.