Impariamo un po' di Rust! Come nostro primo progetto, implementeremo un classico problema di programmazione per principianti: il gioco-indovina. Ecco come funziona: Il nostro programma genererà un intero casuale compreso fra uno e cento. Poi ci chiederà di indovinarlo. Quando avremo inserito un tentativo, ci dirà se siamo stati troppo bassi o troppo alti. Quando indoviniamo, si congratulerà con noi. Mica male, no?
Lungo la strada, impareremo un pochino di Rust. Nel capitolo successivo, ‘Sintassi e semantica’, ci immergeremo in profondità in ogni parte.
Impostiamo un nuovo progetto. Andiamo nella directory dei progetti.
Per il progetto hello_world
abbiamo creato manualmente una struttura
di directory e il file Cargo.toml
. Ma Cargo ha un comando che fa tutto
da solo. Proviamolo:
$ cd ~/projects
$ cargo new gioco_indovina --bin
$ cd gioco_indovina
Abbiamo passato il nome del nostro progetto al comando cargo new
, e poi
il flag --bin
, dato che stiamo creando un programma eseguibile,
invece di una libreria.
Verifichiamo il file Cargo.toml
generato:
[package]
name = "gioco_indovina"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
Cargo ricava queste informazioni dall'ambiente. Se non vanno bene, possono sempre essere corrette.
Infine, Cargo ha generato un file ‘Hello, world!’. Verifichiamo src/main.rs
:
fn main() { println!("Hello, world!"); }
Proviamo a compilare quello che Cargo ci ha fornito:
$ cargo build
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Ottimo! Riapriamo il file src/main.rs
. Scriveremo tutto il nostro codice
in questo file.
Prima di procedere, vediamo un altro comando di Cargo: run
. cargo run
è un po' come cargo build
, ma inoltre lancia l'eseguibile prodotto.
Proviamolo:
$ cargo run
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Running `target/debug/gioco_indovina`
Hello, world!
Benone! Il comando run
torna comodo quando si deve iterare rapidamente
su un progetto. Il nostro gioco è un tale progetto, e ci serve testare
rapidamente ogni iterazione prima di passare alla successiva.
Riprendiamo! La prima cosa che dobbiamo fare per il nostro gioco è
consentire al giocatore di inserire un tentativo. Scriviamo questo nel file
src/main.rs
:
use std::io; fn main() { println!("Indovina il numero!"); println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); println!("Hai digitato: {}", tentativo); }
C'è parecchio qui! Esaminiamolo un pezzo alla volta.
fn main() { use std::io; }use std::io;
Ci servirà prendere l'input dell'utente, e poi stampare il risultato in output.
Pertanto, abbiamo bisogno della libreria io
presa dalla libreria standard.
Di default, Rust importa solamente un po' di cose in ogni programma,
il ‘preludio’. Se non è nel preludio, dovremo importarlo
direttamente, tramite use
. C'è anche un secondo ‘preludio’, il
preludio io
, che serve per uno scopo simile: lo importi, ed
esso importa varie cose utili relative all'io
(operazioni di input-output).
fn main() {
Come abbiamo visto prima, la funzione main()
è il punto di ingresso
del programma. La sintassi fn
dichiara una nuova funzione, le ()
indicano
che non ci sono argomenti, e {
inizia il corpo della funzione. Siccome non
abbiamo specificato un tipo reso, questo si assume essere ()
,
cioè un'ennupla vuota.
println!("Indovina il numero!"); println!("Prego, digita un tentativo.");
Prima abbiamo imparato che println!()
è una macro che stampa
una stringa sullo schermo.
let mut tentativo = String::new();
Adesso si fa interessante! Succedono molte cose in questa breve riga. La prima cosa da notare è che questa è un'istruzione istruzione let, che viene usata per creare ‘legami a variabili’ [variable bindings]. Questi ultimi assumono la forma:
fn main() { let foo = bar; }let foo = bar;
Questa istruzione crea un nuovo legame chiamato foo
, e lo lega al valore
bar
. In molti linguaggi, questo si chiama ‘variabile’, ma i legami
di variabile di Rust hanno alcuni assi nella manica.
Per esempio, di default sono immutabili immutabile. Ecco perché
il nostro esempio usa mut
: rende mutabile il legame, invece che immutabile.
let
non prende un nome sul lato sinistro dell'assegnamento, in realtà
accetta un ‘pattern’. Useremo i pattern più avanti. Per adesso è
abbastanza facile da usare:
let foo = 5; // immutabile. let mut bar = 5; // mutabile
Oh, e //
inizia un commento, che finisce col finire della riga. Rust ignora
tutto il contenuto dei [commenti][commenti].
Perciò adesso sappiamo che let mut tentativo
introduce un legame mutabile
di nome tentativo
, ma dobbiamo guardare dall'altra parte dell'=
per sapere
a che cosa è legato: String::new()
.
String
è un tipo stringa, fornito dalla libreria standard. Una
String
è un pezzo di testo estendibile, codificato in UTF-8.
La sintassi ::new()
usa i ::
perché questa è una ‘funzione associata’
di un particolare tipo. Il che significa che è associata allo stesso tipo
String
, invece che ad una particolare istanza di String
. Altri linguaggi
lo chiamerebbero ‘metodo statico’ o ‘metodo di classe’.
Questa funzione si chiama new()
, perché crea un a nuova String
, vuota.
Molti tipi hanno una funzione new()
, dato che è un nome naturale
per creare un nuovo valore ti qualche tipo.
Andiamo avanti:
fn main() { io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); }io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga");
C'è molto di più! Prendiamo un pezzetto alla volta. La prima riga ha due parti. Ecco la prima:
fn main() { io::stdin() }io::stdin()
Alla prima riga del programma avevamo scritto use std::io
. Adesso stiamo
chiamando una funzione associata a tale tipo. Comunque, se non avessimo scritto
use std::io
, ce la saremmo cavata scrivendo in questa riga
std::io::stdin()
.
Questa funzione particolare restituisce un handle al flusso standard di input per la console. Più specificamente, a std::io::Stdin.
La prossima parte userà questo handle per ottenere l'input dall'utente:
fn main() { .read_line(&mut tentativo) }.read_line(&mut tentativo)
Qui, chiamiamo il metodo read_line()
sul nostro handle.
I metodi sono come funzioni associate, ma sono disponibili solamente
per un'istanza particolare di un tipo, invece che sul tipo stesso. Stiamo anche
passando un argomento a read_line()
: &mut tentativo
.
Come abbiamo legato il nome tentativo
prima? Abbiamo detto che era mutabile.
Però, read_line
non prende una String
come argomento: prende
una &mut String
. Rust ha una caratteristica chiamata ‘riferimenti’, che consente di avere più riferimenti ad un singolo dato,
la quale riduce la necessità di copiare. I riferimenti sono
una caratteristica complessa, dato che uno dei principali vantaggi di Rust
è quanto sia sicuro e facile usare i riferimenti. Però, per adesso, non
ci serve sapere molti dettagli per finire il nostro programma. Per adesso,
l'unica cosa che dobbiamo sapere è che anche i riferimenti, come i legami
let
, sono immutabili di default. Pertanto, dobbiamo scrivere
&mut tentativo
, invece che semplicemente &tentativo
.
Perché read_line()
prende un riferimento mutabile a una stringa? Il suo
compito è prendere i caratteri digitati dall'utente tramite lo standard input,
e collocarli in una stringa. Quindi prende quella come argomento, e per poterci
aggiungere l'input, ha bisogno che sia mutabile.
Ma non abbiamo proprio finito con questa riga di codice. Anche se è una singola riga di testo, è solamente la prima parte della singola riga logica di codice:
fn main() { .expect("Non si riesce a leggere la riga"); }.expect("Non si riesce a leggere la riga");
Quando si chiama un metodo con la sintassi .foo()
, si può andare a capo
o si possono inserire spazi. Ciò aiuta a spezzare righe lunghe. Avremmo anche
potuto scrivere:
io::stdin().read_line(&mut tentativo).expect("non si riesce a leggere la riga");
Ma sarebbe diventato più difficile da leggere. E allora l'abbiamo spezzato;
due righe per due chiamate di metodo. Abbiamo già parlato di read_line()
,
ma che dire di expect()
? Beh, abbiamo già accennato che read_line()
mette
ciò che viene digitato dall'utente nella &mut String
che le passiamo.
Ma restituisce anche un valore: in questo caso, un io::Result
.
Rust ha vari tipi chiamati Result
nella sua libreria standard: un Result
generico, e poi delle versioni specifiche per delle sotto-librerie,
come io::Result
.
Lo scopo di questi tipi Result
è codificare le informazioni di gestione
degli errori. Per i valori del tipo Result
, come per quelli di ogni altro
tipo, sono definiti dei metodi. In questo caso, io::Result
ha un metodo
expect()
che prende un valore su cui è chiamato, e se tale valore
non rappresenta un successo, va in panic!
emettendo il messaggio
che gli è stato passato. Un panic!
come questo manderà in crash il programma,
mostrando il messaggio.
Se omettiamo la chiamata a questo metodo, il nostro programma compilerà, ma otterremo un avvertimento:
$ cargo build
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
src/main.rs:10:5: 10:43 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10 io::stdin().read_line(&mut tentativo);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rust ci avvisa che non abbiamo utilizzato il valore Result
. Questo
avvertimento viene generato da una speciale annotazione utilizzata da io::Result
.
Rust sta cercando di dirci che non abbiamo gestito un possibile errore. Il
modo giusto per sopprimere l'errore è scrivere il codice per la gestione
dell'errore stesso. Fortunatamente, se vogliamo abortire il programma in caso
ci sia un problema, possiamo usare expect()
. Se possiamo recuperare
dall'errore in qualche modo, faremo qualcosa di diverso, ma riserviamo
l'argomento per un progetto futuro.
C'è solo una riga di codice rimasta per questo primo esempio:
fn main() { println!("Hai digitato: {}", tentativo); } }println!("Hai digitato: {}", tentativo); }
Questa riga stampa la stringa dove abbiamo salvato il nostro input. Le
parentesi graffe {}
sono dei segna-posto per passare tentativo
come
argomento. Se avessimo scritto diversi {}
, avremmo passato diversi
argomenti:
let x = 5; let y = 10; println!("x e y: {} e {}", x, y);
Facile.
Possiamo lanciare quello che abbiamo con cargo run
:
$ cargo run
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Running `target/debug/gioco_indovina`
Indovina il numero!
Prego, digita un tentativo.
6
Hai digitato: 6
Tutto bene! La prima parte del progetto è fatta: possiamo prendere input da tastiera, e stamparli a schermo.
Poi, dobbiamo generare un numero segreto. La libreria standard di Rust
non comprende ancora un generatore di numeri casuali. Però, la squadra
di Rust fornisce un crate rand
. Un ‘crate’ ('cassone') è
un pacchetto di codice Rust. Stiamo costruendo un ‘crate binario’, mentre
rand
è un ‘crate libreria’, cioè contiene del codice pronto per essere
usato da altri programmi.
Usare crate esterni è la cosa per cui Cargo fa faville. Prima di poter scrivere
del codice che usa rand
, dobbiamo modificare il nostro Cargo.toml
.
Apriamolo, e aggiungiamo in fondo queste due righe:
[dependencies]
rand="0.3.0"
La sezione [dependencies]
di Cargo.toml
è simile alla sezione [package]
:
ogni cosa che la segue ne fa parte, fino all'inizio della sezione successiva.
Cargo usa la sezione dependencies per sapere quali dipendenze ci sono da crate
esterni, e quali versioni di essi sono richieste. In questo caso, abbiamo
specificato la versione 0.3.0
, che Cargo capisce essere qualunque rilascio
che è compatibile con questa specifica versione. Cargo capisce
il Versionamento semantico, che è uno standard per scrivere
numeri di versione. Un semplice numero come quello sopra è in realtà
un'abbreviazione di ^0.3.0
, che significa "tutte le versioni compatibili
con la versione 0.3.0".
Se avessimo voluto usare solamente proprio la versione 0.3.0
, avremmo potuto
dire rand="=0.3.0"
(si notino i due segni di uguaglianza).
E se volessimo usare sempre la versione più recente, potremmo usare *
.
Potremmo usare anche una gamma di versioni.
La documentazione di Cargo contiene ulteriori dettagli.
Adesso, senza cambiare nient'altro del codice, costruiamo il nostro progetto:
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.8
Downloading libc v0.1.6
Compiling libc v0.1.6
Compiling rand v0.3.8
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
(Naturalmente, si potranno vedere diversi numeri di versione.)
Un sacco di nuovo output! Adesso che abbiamo una dipendenza esterna, Cargo va a prendere le versioni più recenti di ogni cosa dal registry, che è una copia di dati presi da Crates.io. Crates.io è il posto dove la gente nell'ecosistema di Rust invia i suoi progetti open source in Rust per farli usare ad altri.
Dopo aver aggiornato il registry, Cargo verifica la nostra sezione
[dependencies]
e scarica tutti i pacchetti che non abbiamo ancora. In questo
caso, mentre abbiamo detto soltanto che volevamo dipendere da rand
, ci siamo
presi anche una copia di libc
. Questo perché rand
dipende da libc
per funzionare. Dopo averli scaricati, li compila, e poi compila
il nostro progetto.
Se eseguiamo ancora cargo build
, otteniamo un output diverso:
$ cargo build
Va bene, nessun output! Cargo sa che il nostro progetto è stato costruito, e
che tutte le sue dipendenze sono state scaricate e costruite, e quindi non c'è
ragione di fare tutta quella roba. Non avendo niente da fare, semplicemente
termina. Se riapriamo src/main.rs
, facciamo una modifica banale, e lo
salviamo di nuovo, vedremo solamente una riga:
$ cargo build
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Perciò, abbiamo detto a Cargo che volevamo la versione 0.3.x
di rand
, e
quindi ci ha preso la versione più recente che c'era all'istante in cui questo
documento è stato scritto, la v0.3.8
. Ma cosa accadrà la prossima settimana,
quando uscirà la versione v0.3.9
, contenente una correzione importante?
Ricevere le correzioni è importante, ma se la versione 0.3.9
contiene
una regressione incompatibile con il nostro codice?
La risposta a questo problema è il file Cargo.lock
che adesso si troverà
nella directory del nostro progetto. Quando costruiamo un progetto per
la prima volta, Cargo calcola tutti i numeri di versione che soddisfano
i nostri criteri, e poi li scrive nel file Cargo.lock
. Quando si costruisce
nuovamente il progetto, Cargo vedrà che il file Cargo.lock
esiste, e quindi
usa quella specifica versione invece di rifare tutto il lavoro di calcolare
i numeri di versione. Questo consente di avere un build ripetibile automaticamente.
In altri termini, rimarremo alla versione 0.3.8
finché la
aggiorniamo esplicitamente, e così farà chiunque condivida il nostro codice,
grazie al file .lock
.
E che fare quando vogliamo usare la versione v0.3.9
? Cargo ha un altro
comando, update
, che significa ‘ignora il file .lock
, calcola tutte
le versioni più recenti che soddisfano quello che abbiamo specificato.
Se funziona, scrivi quelle versioni nel file .lock
’. Ma, di default, Cargo
cercherà solamente versioni superiori alla 0.3.0
e inferiori alla 0.4.0
. Se volessimo
aggiornare ad una versione 0.4.x
, dovremmo aggiornare direttamente il file
Cargo.toml
. Quando lo facciamo, la prossima volta che eseguiamo il comando
cargo build
, Cargo aggiornerà l'indice e rivaluterà i requisiti del nostro
rand
.
C'è molto più da dire su Cargo e sul suo ecosistema, ma per adesso, è tutto quello che ci serve. Cargo rende davvero facile il riutilizzo delle librerie, e quindi i Rustaciani tendono a scrivere progetti piccoli, assemblati usando vari sotto-progetti.
Procediamo a usare rand
. Ecco il nostro prossimo passo:
extern crate rand; use std::io; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); println!("Hai digitato: {}", tentativo); }
La prima cosa che abbiamo fatto è cambiare la prima riga. Adesso dice
extern crate rand
. Dato che abbiamo dichiarato rand
nelle nostre
[dependencies]
, possiamo usare extern crate
per far sapere a Rust che ne
faremo uso. Questa istruzione fa anche l'equivalente di un use rand;
,
e quindi possiamo fare uso di ogni cosa presente nel crate rand
pur di
qualificarla con il prefisso rand::
.
Poi, abbiamo aggiunto un'altra riga use
: use rand::Rng
. Fra un attimo
useremo un metodo, e tale metodo richiede che Rng
sia nell'ambito del
programma. L'idea
di base è questa: i metodi sono definiti su cose chiamate ‘tratti’ ('traits'),
e affinché
un metodo funzioni, ha bisogno che il suo tratto sia attivo nell'ambito
corrente. Per maggiori dettagli, si legga la sezione sui tratti.
Abbiamo aggiunto due altre righe, nel mezzo:
fn main() { let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); }let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto);
Usiamo la funzione rand::thread_rng()
per ottenere una copia del generatore
di numeri casuali, che è locale al particolare thread di
esecuzione in cui siamo.
Siccome sopra abbiamo scritto use rand::Rng
, adesso il metodo
gen_range()
è disponibile. Questo metodo prende due argomenti, e genera
un numero compreso tra di essi. Il limite inferiore è incluso, mentre il limite
superiore è escluso, e quindi ci servono 1
e 101
per ottenere un numero
che può andare da uno a cento.
La seconda riga stampa il numero segreto. Questa è utile fintanto che stiamo sviluppando il nostro programma, così che possiamo collaudarlo facilmente. Ma la toglieremo per la versione finale. Non sarebbe un gran gioco se stampasse la risposta esatta quando lo si lancia!
Proviamo a eseguire alcune volte il nostro nuovo programma:
$ cargo run
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Running `target/debug/gioco_indovina`
Indovina il numero!
Il numero segreto è: 7
Prego, digita un tentativo.
4
Hai digitato: 4
$ cargo run
Running `target/debug/gioco_indovina`
Indovina il numero!
Il numero segreto è: 83
Prego, digita un tentativo.
5
Hai digitato: 5
Ottimo! Prossimo passo: confrontare il nostro tentativo con il numero segreto.
Adesso che abbiamo l'input dell'utente, confrontiamo il nostro tentativo con il numero segreto. Ecco il nostro nuovo passo, anche se non compila ancora:
extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), } }extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), } }
Ci sono alcune aggiunte. La prima è un altro use
. Portiamo nell'ambito
un tipo chiamato std::cmp::Ordering
. Poi, ci sono cinque nuove righe
in fondo che usano tale tipo:
match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), }
Il metodo cmp()
può essere chiamato su qualunque oggetto che può essere
confrontato, e prende un riferimento all'oggetto con cui lo si vuole
confrontare. Il confronto restituisce il tipo Ordering
che abbiamo importato prima. Usiamo
un'istruzione match
per determinare esattamente quale Ordering
abbiamo ottenuto dal confronto. Ordering
è una enum
, abbreviazione
di ‘enumerazione’. Le enumerazioni si presentano così:
enum Foo { Bar, Baz, }
Con questa definizione, ogni oggetto di tipo Foo
può essere o una Foo::Bar
o un Foo::Baz
. Usiamo il ::
per indicare lo spazio dei nomi
di una paricolare variante della enum
.
L'enum
Ordering
ha tre possibili varianti: Less
, Equal
,
e Greater
. L'istruzione match
prende un valore di un tipo, e permette
di creare un ‘braccio’ per ogni valore possibile. Dato che abbiamo tre varianti
di Ordering
, dobbiamo avere tre bracci:
match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), }
Se vale Less
, stampiamo Troppo piccolo!
, se vale Greater
,
Troppo grande!
, e se vale Equal
, Hai vinto!
. match
è davvero utile,
ed è usato spesso in Rust.
Ho già detto che questo codice non compilava, però. Proviamolo:
$ cargo build
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
src/main.rs:28:25: 28:40 error: mismatched types:
expected `&collections::string::String`,
found `&_`
(expected struct `collections::string::String`,
found integral variable) [E0308]
src/main.rs:28 match tentativo.cmp(&numero_segreto) {
^~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `gioco_indovina`.
Urca! Questo è un grosso errore. Il sua sintesi è che abbiamo dei ‘tipi male
accoppiati’ ["‘mismatched types’"]. Rust ha un sistema dei tipi forte
e statico. Però, ha anche l'inferenza dei tipi. Quando abbiamo scritto
let tentativo = String::new()
, Rust è stato capace di inferire che
tentativo
doveva essere una String
, e quindi non ci ha costretto
a scriverne il tipo. E con il nostro numero_secreto
, ci sono vari tipi
che possono avere un valore fra uno e cento: i32
, cioè un numero intero
con segno a trentadue bit, o u32
, cioè un numero intero senza segno
a trentadue bit, o i64
, un numero intero con segno a sessantaquattro bit
oppure altri. Finora questo fatto non ha importato, e quindi Rust ha preso come default
un i32
. Però, qui, Rust non sa come confrontare tentativo
con
secret_number
. Devono essere dello stesso tipo. Pertanto, dobbiamo convertire
la String
che abbiamo letto come input in un vero tipo numerico, per poterlo
confrontare. Lo possiamo fare aggiungendo due righe. Ecco il nostro nuovo
programma:
extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); let tentativo: u32 = tentativo.trim().parse() .expect("Prego, digita un numero!"); println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), } }
Le due nuove righe:
fn main() { let tentativo: u32 = tentativo.trim().parse() .expect("Prego, digita un numero!"); }let tentativo: u32 = tentativo.trim().parse() .expect("Prego, digita un numero!");
Aspetta un attimo, pensavo che avevamo già un tentativo
? Sì, ma Rust ci
permette di ’oscurare’ ["‘shadow’"] il precedente tentativo
con uno nuovo.
Questo si usa spesso proprio in questa situazione, in cui tentativo
inizia
come String
, ma lo vogliamo convertire in un u32
. L'oscuramento ci permette
di riusare il nome tentativo
, invece di costringerci a inventare due nomi
diversi come tentativo_str
e tentativo
, o qualcos'altro.
Leghiamo tentativo
ad una espressione che somiglia a qualcosa che abbiamo
scritto prima:
tentativo.trim().parse()
Qui, tentativo
si riferisce al vecchio tentativo
, quello che era
una String
contenente il nostro input. Il metodo trim()
applicato ad
una String
eliminerà tutti gli spazi all'inizio e alla fine della stringa.
Questo è importante, dato che abbiamo dovuto premere il tasto ‘Invio’
per soddisfare la funzione read_line()
. Ciò significa che se digitiamo 5
e battiamo Invio, tentativo
conterrà 5\n
. La stringa \n
rappresenta un
carattere ‘a capo’, inserito dal tasto Invio. trim()
se ne sbarazza,
lasciando nella nostra stringa solamente il 5
. Il metodo parse()
applicato
a una stringa analizza la stringa estraendone un numero di qualche
tipo. Dato che tale metodo può riconoscere vari tipi di numeri, dobbiamo
suggerire a Rust il tipo esatto del numero che vogliamo. Pertanto, scriviamo
let tentativo: u32
. I due-punti (:
) dopo tentativo
dicono a Rust
che stiamo annotando il tipo del legame. u32
è il tipo intero senza segno
a trentadue bit. Rust ha vari tipi numerici predefiniti, ma abbiamo
scelto u32
. È una buona scelta di default per un numero positivo piccolo.
Proprio come read_line()
, anche la nostra chiamata a parse()
potrebbe
provocare un errore. Che fare se la nostra stringa contenesse A👍%
? Non ci
sarebbe modo di convertirla in un numero. Pertanto, faremo la stessa cosa che
abbiamo fatto con read_line()
: usiamo il metodo expect()
per andare
in crash se c'è un errore.
Proviamo il nostro programma!
$ cargo run
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Running `target/gioco_indovina`
Indovina il numero!
Il numero segreto è: 58
Prego, digita un tentativo.
76
Hai digitato: 76
Troppo grande!
Carino! Si può vedere che abbiamo perfino aggiunto degli spazi prima di digitare il tentativo, ma il programma è comunque riuscito a valutarlo come 76. Eseguiamo il programma alcune volte, e verifichiamo che funzioni quando si indovina il numero, e anche quando il tentativo è un numero troppo piccolo.
Adesso la maggior parte del gioco funziona, ma possiamo fare un solo tentativo. Modifichiamolo aggiungendo i cicli!
La parola-chiave loop
ci dà un ciclo infinito. Aggiungiamola:
extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); loop { println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); let tentativo: u32 = tentativo.trim().parse() .expect("Prego, digita un numero!"); println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), } } }
E proviamola. Ma, un momento, non abbiamo appena aggiunto un ciclo infinito?
Già. Prima, parlando di parse()
, abbiamo detto che se gli diamo una risposta
che non è un numero, andrà in panic!
e terminerà. Osserviamo:
$ cargo run
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Running `target/gioco_indovina`
Indovina il numero!
Il numero segreto è: 59
Prego, digita un tentativo.
45
Hai digitato: 45
Troppo piccolo!
Prego, digita un tentativo.
60
Hai digitato: 60
Troppo grande!
Prego, digita un tentativo.
59
Hai digitato: 59
Hai vinto!
Prego, digita un tentativo.
basta
thread 'main' panicked at 'Prego, digita un numero!'
Ah! Digitando basta
in effetti si termina il programma. Come fa qualunque
input non numerico. Beh, questo non è il massimo, a dir poco. Come prima cosa,
terminiamo effettivamente quando si vince il gioco:
extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); loop { println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); let tentativo: u32 = tentativo.trim().parse() .expect("Prego, digita un numero!"); println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => { println!("Hai vinto!"); break; } } } }
Aggiungendo la riga break
dopo la stampa di Hai vinto!
, usciremo dal ciclo
quando si vince. Uscire dal ciclo comporta anche uscire dal programma, dato che
non c'è altro in main()
. Abbiamo solamente un altro ritocco da fare: quando
si digita un input non numerico, non vogliamo terminare, vogliamo ignorarlo.
Lo possiamo fare così:
extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); println!("Il numero segreto è: {}", numero_segreto); loop { println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); let tentativo: u32 = match tentativo.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => { println!("Hai vinto!"); break; } } } }
Le righe modificate sono le seguenti:
fn main() { let tentativo: u32 = match tentativo.trim().parse() { Ok(num) => num, Err(_) => continue, }; }let tentativo: u32 = match tentativo.trim().parse() { Ok(num) => num, Err(_) => continue, };
Il modo tipico per passare da un ‘crash dovuto a errore’ a una ‘gestione
effettiva dell'errore’, è passare dall'uso del metodo expect()
all'uso
dell'istruzione match
. La chiamata a parse()
restituisce un Result
;
questo è una enum
, come Ordering
, ma in questo caso,
ogni variante ha alcuni dati associati ad essa:
Ok
indica un successo, e Err
indica un fallimento, ma
entrambi contengono ulteriori informazioni: per il primo, l'intero estratto
con successo, e per l'altro il genere di errore. In questo caso, match
proverà a far combaciare il suo argomento con Ok(num)
, che assegnerà al nome
il valore contenuto in Ok
(cioè l'intero estratto), e restituirà
quest'ultimo nel lato destro. Nel caso Err
, non ci interessa il genere
di errore, e così usiamo il carattere piglia-tutto _
, invece di un nome.
Questo jolly combacia con qualunque codice d'errore, e poi continue
ci
sposterà alla prossima iterazione del ciclo; in effetti, questo codice
ci consente di ignorare tutti gli errori e continuare nel programma.
Adesso dovremmo essere a posto! Proviamo:
$ cargo run
Compiling gioco_indovina v0.1.0 (file:///home/you/projects/gioco_indovina)
Running `target/gioco_indovina`
Indovina il numero!
Il numero segreto è: 61
Prego, digita un tentativo.
10
Hai digitato: 10
Troppo piccolo!
Prego, digita un tentativo.
99
Hai digitato: 99
Troppo grande!
Prego, digita un tentativo.
foo
Prego, digita un tentativo.
61
Hai digitato: 61
Hai vinto!
Magnifico! Con un ultimo ritocchino, finiremo il gioco. Riesci ad indovinare quale sarà? Giusto, non vogliamo stampare subito il numero segreto. Andava bene per il collaudo, ma rovinava il gioco. Ecco il nostro sorgente finale:
extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); loop { println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); let tentativo: u32 = match tentativo.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => { println!("Hai vinto!"); break; } } } }extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { println!("Indovina il numero!"); let numero_segreto = rand::thread_rng().gen_range(1, 101); loop { println!("Prego, digita un tentativo."); let mut tentativo = String::new(); io::stdin().read_line(&mut tentativo) .expect("Non si riesce a leggere la riga"); let tentativo: u32 = match tentativo.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("Hai digitato: {}", tentativo); match tentativo.cmp(&numero_segreto) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => { println!("Hai vinto!"); break; } } } }
Questo progetto ha mostrato parecchie cose: let
, match
, i metodi,
le funzioni associate, l'uso di crate esterni, e altro.
A questo punto, abbiamo costruito un gioco-indovina funzionante! Congratulazioni!