Programmare un Gioco di Indovinelli

Immergiamoci in Rust lavorando a un progetto pratico insieme! Questo capitolo ti introduce ad alcuni concetti comuni di Rust mostrandoti come usarli in un programma reale. Imparerai a conoscere let, match, metodi, funzioni associate, crate esterni e altro ancora! Nei capitoli seguenti, esploreremo questi concetti in dettaglio. In questo capitolo, ti eserciterai solo sui fondamentali.

Implementeremo un classico problema di programmazione per principianti: un gioco di indovinelli. Ecco come funziona: il programma genererà un numero intero casuale tra 1 e 100. Quindi inviterà il giocatore a inserire un indovinello. Dopo che è stato inserito, il programma indicherà se l’indovinello è troppo basso o troppo alto. Se l’indovinello è corretto, il gioco stamperà un messaggio di congratulazioni e uscirà.

Configurare un Nuovo Progetto

Per configurare un nuovo progetto, vai nella directory projects che hai creato nel Capitolo 1 e crea un nuovo progetto utilizzando Cargo, così:

$ 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!” e eseguiamolo nello stesso passo 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 velocemente ogni iterazione prima di passare a quella 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à tale input e controllerà che l'input sia nella forma prevista. Per iniziare, permetteremo al giocatore di inserire un indovinello. Inserisci il codice nel Listing 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 quindi stampare il risultato come output, dobbiamo importare la libreria di input/output io 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 impostazione predefinita, Rust ha un insieme di elementi definiti nella libreria standard che include nello Scope di ogni programma. Questo insieme è chiamato prelude, e puoi vedere tutto ciò che contiene nella documentazione della libreria standard.

Se il tipo che vuoi usare non è nel prelude, devi importerlo esplicitamente nello Scope con un'istruzione use. Usare la libreria std::io ti fornisce un numero di funzionalità utili, inclusa la possibilità di accettare l'input dell'utente.

Come hai visto nel Capitolo 1, la funzione main è il punto d'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, (), indicano che non ci sono parametri; e le parentesi graffe, {, iniziano il corpo della funzione.

Come hai anche imparato nel Capitolo 1, println! è una macro che stampa una stringa sullo 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 indica qual è il gioco e richiede input dall’utente.

Memorizzare i Valori con le Variabili

Successivamente, creeremo una variabile per memorizzare l’input dell’utente, come questo:

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 sta diventando interessante! C’è molto in questa piccola riga. Usiamo l'istruzione let per creare la variabile. Ecco un altro esempio:

let apples = 5;

Questa riga crea una nuova variabile chiamata apples e la lega al valore 5. In Rust, le variabili sono immutabili per impostazione predefinita, il che significa che una volta assegnato il valore alla variabile, il 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 riga. Rust ignora tutto nei commenti. Discuteremo i commenti in modo più dettagliato nel Capitolo 3.

Ritornando al programma di gioco di indovinelli, ora sai che let mut guess introdurrà una variabile mutabile chiamata guess. Il segno di uguale (=) dice a Rust che vogliamo legare qualcosa alla variabile ora. A destra del segno di uguale c'è il valore a cui guess è legato, che è 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 espandibile.

La sintassi :: nella riga ::new indica che new è una funzione associata al 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 riga let mut guess = String::new(); ha creato una variabile mutabile che è attualmente legata a una nuova istanza vuota di una String. Uffa!

Ricevere Input dall’Utente

Ricorda che abbiamo incluso la funzionalità di input/output dalla libreria standard con use std::io; nella prima riga del programma. Ora chiameremo la funzione stdin dal modulo io, che 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 all'input standard per il tuo terminale.

Successivamente, la riga .read_line(&mut guess) chiama il metodo read_line sull'handle dell'input standard per ottenere l'input dall'utente. Stiamo anche passando &mut guess come argomento a read_line per dirgli in quale stringa memorizzare l’input dell’utente. Il compito completo di read_line è prendere qualsiasi cosa l’utente digiti nell’input standard e aggiungerla a una stringa (senza sovrascriverne i contenuti), quindi passiamo quella stringa come argomento. L’argomento della stringa deve essere mutabile affinché il metodo possa cambiare il contenuto della stringa.

Il & indica che questo argomento è un reference, il che ti dà un modo per permettere a più parti del tuo codice di accedere a un pezzo di dati senza dover copiare quei dati in memoria più volte. I reference sono una caratteristica complessa, e uno dei grandi vantaggi di Rust è quanto sia sicuro e facile usare i reference. Non hai bisogno di sapere molti di questi dettagli per finire questo programma. Per ora, tutto ciò che devi sapere è che, come le variabili, i reference sono immutabili per impostazione predefinita. Pertanto, devi scrivere &mut guess anziché &guess per renderlo mutabile. (Il Capitolo 4 spiegherà i reference più approfonditamente.)

Gestione di Potenziali Errori con Result

Stiamo ancora lavorando su questa riga di codice. Stiamo ora discutendo una terza riga di testo, ma nota che fa ancora parte di una singola riga logica di codice. La parte successiva è 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'interruzione di riga e altri spazi bianchi per aiutare a suddividere le righe lunghe quando si chiama un metodo con la sintassi .method_name(). Ora discutiamo cosa fa questa riga.

Come menzionato prima, read_line inserisce qualsiasi cosa l’utente inserisca nella stringa che gli passiamo, ma restituisce anche un valore Result. Result è una enumerazione, spesso chiamata enum, che è un tipo che può essere in uno dove multiple possibili stati. Chiamiamo ciascun possibile stato una variante.

Il Capitolo 6 coprirà le enum in modo più dettagliato. Lo scopo di questi tipi Result è codificare informazioni sulla gestione degli errori.

Le varianti di Result sono Ok e Err. La variante Ok indica che l'operazione è stata eseguita con successo e all'interno di 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.

I valori del tipo Result, come i valori di qualsiasi tipo, hanno metodi definiti su di essi. Un'istanza di Result ha un metodo expect che puoi chiamare. Se questa istanza di Result è un valore Err, expect causerà il crash del programma e visualizzerà il messaggio che hai passato come argomento a expect. Se il metodo read_line restituisce un Err, probabilmente sarà il risultato di un errore proveniente dal sistema operativo sottostante. Se questa istanza di Result è un valore Ok, expect prenderà il valore restituito che Ok sta mantenendo e restituirà solo quel valore 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 verrà compilato, ma riceverai un avviso:

$ 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 usato il valore Result restituito da read_line, indicando che il programma non ha gestito un possibile errore.

Il modo giusto per sopprimere l’avviso è scrivere effettivamente il codice per la gestione degli errori, ma nel nostro caso vogliamo solo che questo programma vada in crash quando si verifica un problema, quindi possiamo usare expect. Imparerai a gestire il recupero dagli errori nel Capitolo 9.

Stampare Valori con i Segnaposto 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 a piccole chele di granchio che tengono in posizione un valore. Quando stampi il valore di una variabile, il nome della variabile può andare all'interno delle parentesi graffe. Quando stampi il risultato della valutazione di un’espressione, inserisci parentesi graffe vuote nella stringa di formato, poi segui la stringa di formato con una lista di espressioni separate da virgole da stampare in ogni segnaposto di parentesi graffe vuote nello stesso ordine. Stampare una variabile e il risultato di un’espressione in una chiamata a println! sembrerebbe 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`
Indovina il numero!
Per favore, inserisci il tuo indovinello.
6
Hai indovinato: 6

A questo punto, la prima parte del gioco è fatta: stiamo ottenendo l’input dalla tastiera e poi lo stiamo stampando.

Generare un Numero Segreto

Successivamente, dobbiamo generare un numero segreto che l’utente tenterà di indovinare. Il numero segreto dovrebbe essere diverso ogni volta in modo che il gioco sia divertente da giocare più di una volta. Utilizzeremo un numero casuale tra 1 e 100 in modo che il gioco non sia troppo difficile. Rust non include ancora la funzionalità per i numeri casuali nella sua libreria standard. Tuttavia, il team di Rust fornisce un crate rand con quella funzionalità.

Utilizzare un Crate per Ottenere Più Funzionalità

Ricorda che un crate è una raccolta di file di codice sorgente Rust. Il progetto che abbiamo costruito è un crate binario, che è un eseguibile. Il crate rand è un crate libreria, che contiene codice destinato a essere usato in altri programmi e non può essere eseguito da solo.

Il coordinamento di Cargo con i crate esterni è dove Cargo brilla davvero. Prima di poter scrivere codice che utilizzi 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 questa versione, altrimenti gli esempi di codice 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 di 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 i numeri di versione. Lo specificatore 0.8.5 è in realtà una scorciatoia per ^0.8.5, il che significa qualsiasi versione 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 assicura che otterrai l'ultima release della patch che continuerà a compilare con il codice in questo capitolo. Qualsiasi versione 0.9.0 o superiore non è garantita di avere la stessa API di quanto usato nei seguenti esempi.

Ora, senza modificare alcun 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 comunque 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 preleva le versioni più recenti di tutto ciò che quella dipendenza necessita dal registro, che è una copia di 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 sia già stato scaricato. In questo caso, anche se abbiamo elencato solo rand come dipendenza, Cargo ha anche preso altri crate di 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 fare alcuna modifica, non otterrai alcun output oltre alla riga Finished. Cargo sa che ha già scaricato e compilato le dipendenze e non hai cambiato nulla nel tuo file Cargo.toml. Cargo sa anche che non hai cambiato nulla del tuo codice, quindi non ricompila neanche quello. Non avendo nulla da fare, semplicemente esce.

Se apri il file src/main.rs, fai una modifica banale e poi lo salvi e lo ricostruisci, 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 il tuo piccolo cambiamento 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 assicura che tu possa ricostruire lo stesso artefatto ogni volta che tu o qualcun altro costruite il tuo codice: Cargo utilizzerà solo le versioni delle dipendenze che hai specificato finché non indicherai diversamente. Per esempio, supponiamo che la prossima settimana esca la versione 0.8.6 del crate rand, e che quella versione contenga una correzione importante di un bug, ma contenga anche una regressione che romperà il tuo codice. Per gestire questo, 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 permette di avere una build riproducibile automaticamente. In altre parole, il tuo progetto rimarrà alla versione 0.8.5 finché non esegui un aggiornamento esplicito, grazie al file Cargo.lock. Poiché il file Cargo.lock è importante per build riproducibili, spesso è incluso nel controllo del codice sorgente insieme al resto del codice del tuo progetto.

Aggiornare un Crate per Ottenere una Nuova Versione

Quando vuoi aggiornare un crate, Cargo fornisce il comando update, che ignorerà il file Cargo.lock e determinerà tutte le versioni più recenti che soddisfano le tue specifiche in Cargo.toml. Cargo scriverà quindi 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 la release 0.9.0. A questo punto, noteresti anche una modifica nel file Cargo.lock che indica che la versione del crate rand che stai ora utilizzando è la 0.8.6. Per utilizzare la versione 0.9.0 di rand o qualsiasi versione nella serie 0.9.x, dovresti aggiornare il file Cargo.toml per assomigliare a questo:

[dependencies]
rand = "0.9.0"

La prossima volta che esegui cargo build, Cargo aggiornerà il registro dei crate disponibili e rivaluterà i tuoi requisiti di rand in base alla nuova versione che hai specificato.

C'è molto altro da dire su Cargo e [il suo ecosistema][doccratesio], di cui discuteremo nel Capitolo 14, ma per ora, questo è tutto quello che devi sapere. Cargo rende molto facile riutilizzare le librerie, quindi i Rustaceans possono scrivere progetti più piccoli che sono assemblati da un numero di pacchetti.

Generare un Numero Casuale

Iniziamo a usare rand per generare un numero da indovinare. Il prossimo passo è 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 riga use rand::Rng;. Il trait Rng definisce i metodi che i generatori di numeri casuali implementano, e questo trait deve essere nello scope affinché possiamo usare quei metodi. Il Capitolo 10 coprirà i trait in dettaglio.

Successivamente, aggiungiamo due righe nel mezzo. Nella prima riga, chiamiamo la funzione rand::thread_rng che ci dà il particolare generatore di numeri casuali che useremo: 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 usando qui prende la forma start..=end ed è inclusivo sui limiti inferiori e superiori, quindi dobbiamo specificare 1..=100 per richiedere un numero tra 1 e 100.

Nota: Non saprai solo quali trait usare e quali metodi e funzioni chiamare da un crate, quindi ogni crate ha documentazione con istruzioni per usarlo. Un'altra caratteristica interessante di Cargo è che eseguendo il comando cargo doc --open verrà costruita la documentazione fornita da tutte le tue dipendenze localmente e aperta nel tuo browser. Se sei interessato ad altre funzionalità nel crate rand, per esempio, esegui cargo doc --open e fai clic su rand nella barra laterale a sinistra.

La seconda nuova riga stampa il numero segreto. Questo è utile mentre stiamo sviluppando il programma per poterlo testare, ma lo cancelleremo dalla versione finale. Non è molto un gioco se il programma stampa la risposta non 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`
Indovina il numero!
Il numero segreto è: 83
Per favore inserisci il tuo indovinello.
5
Hai indovinato: 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. Questo passaggio è mostrato nel Listing 2-4. Nota che questo codice non compila ancora, 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 fondo che utilizzano il tipo Ordering. Il metodo cmp confronta due valori e può essere chiamato su qualsiasi cosa possa essere confrontata. Prende un riferimento a ciò con cui vuoi confrontare: qui sta confrontando guess con il 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 è composta da braccia. Un braccio 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 braccio. Rust prende il valore dato a match e guarda attraverso il pattern di ciascun braccio a turno. I pattern e il costrutto match sono potenti funzionalità di Rust: ti permettono di esprimere una varietà di situazioni che il tuo codice potrebbe incontrare e ti assicurano di gestirle tutte. Queste funzionalità saranno trattate in dettaglio nel Capitolo 6 e nel Capitolo 18, rispettivamente.

Esaminiamo un esempio con l'espressione match che usiamo qui. Supponiamo che l'utente abbia indovinato 50 e 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 controllare il pattern di ciascun braccio. Guarda il pattern del primo braccio, Ordering::Less, e vede che il valore Ordering::Greater non corrisponde a Ordering::Less, quindi ignora il codice in quel braccio e passa al prossimo braccio. Il pattern del prossimo braccio è Ordering::Greater, che corrisponde a Ordering::Greater! Il codice associato in quel braccio verrà eseguito e stampa Too big! sullo schermo. L'espressione match termina dopo il primo abbinamento riuscito, quindi non guarderà l'ultimo braccio in questo scenario.

Tuttavia, il codice nel Listing 2-4 non compila ancora. 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 di tipi forte e statico. Tuttavia, ha anche l'inferenza dei tipi. Quando abbiamo scritto let mut guess = String::new(), Rust è stato in grado di inferire che guess dovrebbe essere una String e non ci ha fatto scrivere il tipo. Il secret_number, d'altra parte, è un tipo numerico. Alcuni dei tipi numerici di Rust possono avere un valore tra 1 e 100: i32, un numero a 32 bit; u32, un numero senza segno a 32 bit; i64, un numero a 64 bit; così come altri. Se non diversamente specificato, Rust predefinito è uno 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.

In definitiva, vogliamo convertire la String che il programma legge come input in un tipo numerico in modo da poterla confrontare numericamente con il numero segreto. Lo facciamo aggiungendo questa riga al corpo della funzione main:

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}");

    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 riga è:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

Creiamo una variabile chiamata guess. Ma aspetta, il programma non ha già una variabile chiamata guess? Sì, ma fortunatamente Rust ci permette di ombreggiare il valore precedente di guess con uno nuovo. L'ombreggiatura ci permette di riutilizzare il nome della variabile guess piuttosto che costringerci a creare due variabili uniche, come guess_str e guess, per esempio. Copriremo questo in maggior dettaglio nel Capitolo 3, ma per ora, sappi che questa funzionalità è spesso utilizzata quando vuoi convertire un valore da un tipo a un altro tipo.

Abbiamo associato questa nuova variabile all'espressione guess.trim().parse(). Il guess nell'espressione si riferisce alla variabile guess originale che conteneva l'input come una 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 proprio guess, il che aggiunge un carattere di nuova linea alla stringa. Ad esempio, se l'utente digita 5 e preme invio, guess apparirà così: 5\n. Il \n rappresenta "nuova linea". (Su Windows, premendo invio si genera un ritorno a capo e una nuova linea, \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 il tipo di numero esatto che vogliamo usando let guess: u32. I due punti (:) dopo guess indicano a Rust che annoteremo il tipo della variabile. Rust ha diversi tipi di numeri integrati; il u32 visto qui è un intero senza segno a 32 bit. È una buona scelta predefinita per un numero positivo piccolo. Imparerai altri tipi di numeri nel Capitolo 3.

Inoltre, l'annotazione u32 in questo programma di esempio e il confronto con secret_number implica che Rust dedurrà che secret_number dovrebbe essere un u32 anche. Quindi ora il confronto sarà tra due valori dello stesso tipo!

Il metodo parse funzionerà solo su caratteri che possono essere logicamente convertiti in numeri e quindi può facilmente causare errori. Se, per esempio, la stringa conteneva A👍%, non ci sarebbe modo di convertire ciò 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 Potenziali Errori con Result). Tratteremo questo Result allo stesso modo usando nuovamente il metodo expect. Se parse restituisce una variante Err del Result perché non è riuscita a creare un numero dalla stringa, la chiamata expect farà crollare il gioco e stampare il messaggio che gli diamo. Se parse riesce a convertire correttamente la stringa in un numero, restituirà la variante Ok del Result, e expect restituirà il numero che vogliamo dal valore Ok.

Eseguiamo ora il programma:

$ 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!

Fantastico! Anche se sono stati aggiunti spazi prima del guess, il programma ha comunque capito che l'utente ha guessato 76. Esegui il programma alcune volte per verificare il diverso comportamento con diversi tipi di input: indovinare il numero correttamente, indovinare un numero troppo alto e indovinare un numero troppo basso.

Abbiamo la maggior parte del gioco che funziona ora, ma l'utente può fare solo un guess. Cambiamo ciò aggiungendo un loop!

Consentire Molteplici Guess con il Looping

La parola chiave 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 dall'invito a inserire il guess in avanti all'interno di un loop. Assicurati di indentare le righe all'interno del loop di altri quattro spazi ciascuna e esegui di nuovo il programma. Il programma ora chiederà un altro guess per sempre, il che in realtà introduce un nuovo problema. Sembra che l'utente non possa smettere!

L'utente potrebbe sempre interrompere il programma utilizzando la scorciatoia da tastiera ctrl-c. Ma c'è un altro modo per sfuggire a questo mostro insaziabile, come menzionato nella discussione sul parse in “Confrontare il Guess con il Numero Segreto”: se l'utente inserisce una risposta non numerica, il programma si bloccherà. Possiamo sfruttare ciò per permettere 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`
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

Digitare quit uscirà dal gioco, ma come noterai, così farà anche l'inserimento di qualsiasi altro input non numerico. Questo è subottimale, per non dire altro; vogliamo che il gioco si fermi anche quando il numero corretto è indovinato.

Uscire Dopo un Indovinare Corretto

Programmiamo il gioco per uscire quando l'utente vince aggiungendo un'istruzione 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 You win! fa uscire il programma dal loop quando l'utente indovina correttamente il numero segreto. Uscire dal loop significa anche uscire dal programma, poiché il loop è l'ultima parte di main.

Gestire l'Input Non Valido

Per perfezionare ulteriormente il comportamento del gioco, invece di far crollare il programma quando l'utente inserisce un input non numerico, facciamo in modo che il gioco ignori l'input non numerico in modo che l'utente possa continuare a indovinare. Possiamo farlo modificando la riga in cui guess viene 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 dall'usare un crash per un errore a gestire l'errore. Ricorda che parse restituisce un tipo Result e Result è un enum che ha le varianti Ok e Err. Qui usiamo un'espressione match, come abbiamo fatto con il risultato Ordering del metodo cmp.

Se parse è in grado di trasformare con successo la stringa in un numero, restituirà un valore Ok che contiene il numero risultante. Quel valore Ok corrisponderà al pattern del primo braccio, e l'espressione match restituirà semplicemente il valore num che parse ha prodotto e messo all'interno del 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 braccio del match, ma corrisponde al pattern Err(_) nel secondo braccio. Il trattino basso, _, è un valore jolly; in questo esempio, stiamo dicendo che vogliamo abbinare tutti i valori Err, indipendentemente dalle informazioni che hanno dentro. Quindi il programma eseguirà il codice del secondo braccio, continue, che dice al programma di andare alla prossima iterazione del loop e chiedere un altro guess. Quindi, effettivamente, il programma ignora 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`
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!

Fantastico! Con un piccolo ritocco finale, completeremo il gioco di indovina. Richiama che il programma sta ancora stampando il numero segreto. Funzionava bene per i test, ma rovina il gioco. Cancelliamo il println! che produce 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 di indovina. 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 próximos due capitoli, imparerai questi concetti in modo più dettagliato. 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 la proprietà, una caratteristica che rende Rust diverso dagli altri linguaggi. Il Capitolo 5 discute gli struct e la sintassi dei metodi, e il Capitolo 6 spiega come funzionano gli enum.