Programarea unui joc de ghicit
Să ne avântăm în Rust lucrând împreună la un proiect direct aplicabil! În acest capitol o să te familiarizezi cu câteva concepte uzitate în Rust, arătându-ți cum să le aplici într-un program concret. Îți vei însuși cunoștințe despre let
, match
, metode, funcții asociate, crate-uri externe și mai mult! În capitolele ce urmează, vom aprofunda aceste idei. Pentru moment, ne vom concentra pe exersarea noțiunilor fundamentale.
Vom pune în aplicare o problemă emblematică pentru începătorii în programare: un joc de ghicit. Iată în ce constă: programul va genera un număr întreg aleatoriu între 1 și 100. Va solicita apoi jucătorului să introducă o ghicire. După ce un număr a fost introdus, programul va specifica dacă acesta este prea mic sau prea mare. Dacă răspunsul este corect, jocul va afișa un mesaj de felicitare și se va închide.
Formarea unui proiect nou
Pentru a forma un proiect nou, mergi la directoriul proiecte pe care l-ai creat în Capitolul 1 și fă un proiect nou folosind Cargo, astfel:
$ cargo new guessing_game
$ cd guessing_game
Prima comandă, cargo new
, ia numele proiectului (guessing_game
) ca prim argument. A doua comandă trece la directoriul noului proiect.
Aruncă o privire la fișierul Cargo.toml generat:
Numele fișierului: 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]
După cum ai văzut în Capitolul 1, cargo new
generează un program “Salut, lume!” pentru tine. Verifică fișierul src/main.rs:
Numele fișierului: src/main.rs
fn main() { println!("Hello, world!"); }
Acum să compilăm acest program „Salut, lume!" și să îl rulăm în același pas folosind comanda 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!
Comanda run
este utilă când ai nevoie să iterezi rapid pe un proiect, așa cum vom face în acest joc, testând rapid fiecare iterație înainte de a trece la următoarea.
Deschide din nou fișierul src/main.rs. Vei scrie tot codul în acest fișier.
Procesarea unei ghiciri
Prima parte a programului de ghicit va cere intrarea utilizatorului, va procesa acea intrare și va verifica dacă intrarea este în forma așteptată. Pentru început, vom permite jucătorului să introducă o ghicire. Introdu codul din Listare 2-1 în src/main.rs.
Numele fișierului: 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}");
}
Lstarea 2-1: Cod care obține o ghicire de la utilizator și o tipărește
Acest cod conține o mulțime de informații, deci să-l parcurgem linie cu linie. Pentru a obține intrarea utilizatorului și apoi a printa rezultatul ca ieșire, avem nevoie să aducem biblioteca io
de intrare/ieșire în scopul nostru. Biblioteca io
vine din biblioteca standard, cunoscută sub numele de 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}");
}
În mod implicit, Rust are un set de elemente definite în librăria standard pe care le introduce în domeniul de vizibilitatea al fiecărui program. Acest set se numește prelude, și poți vedea tot ce se află în el în documentația librăriei standard.
Dacă un tip pe care dorești să-l folosești nu se află în prelude, trebuie să introduci acel tip explicit în domeniu cu o instrucțiune use
. Utilizarea librăriei std::io
îți oferă un număr de caracteristici utile, inclusiv capacitatea de a accepta intrarea utilizatorului.
După cum ai văzut în Capitolul 1, funcția main
este punctul de intrare în program:
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}");
}
Sintaxa fn
declară o nouă funcție; parantezele, ()
, indică faptul că nu există parametri; iar paranteza rotundă, {
, începe corpul funcției.
Așa cum ai învățat și în Capitolul 1, println!
este o macrocomandă care tipărește un string pe ecran:
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}");
}
Acest cod afișează un prompt care indică ce joc este și solicită intrare de la utilizator.
Păstrarea valorilor cu variabile
În continuare, vom crea o variabilă pentru a stoca input-ul utilizatorului, astfel:
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}");
}
Acum programul devine interesant! Se întâmplă multe în această mică linie. Folosim declarația let
pentru a crea variabila. Iată un alt exemplu:
let apples = 5;
Această linie de cod creează o nouă variabilă numită apples
și o leagă de valoarea 5. În Rust, variabilele sunt imutabile în mod implicit, ceea ce înseamnă că odată ce dăm variabilei o valoare, valoarea nu se va schimba. Vom discuta acest concept în detaliu în secțiunea „Variabile și mutabilitate” din Capitolul 3. Pentru a face o variabilă mutabilă, adăugăm mut
înainte de numele variabilei:
let apples = 5; // imutabil
let mut bananas = 5; // mutabil
Notă: Sintaxa
//
începe un comentariu care continuă până la sfârșitul liniei. Rust ignoră totul în comentarii. Vom discuta comentariile în mai multe detalii în Capitolul 3.
Revenind la programul de ghicit, acum știi că let mut guess
va introduce o variabilă mutabilă denumită guess
. Semnul egal (=
) spune lui Rust că dorim să legăm ceva la variabilă acum. Pe dreapta semnului egal se află valoarea la care guess
este legată, care este rezultatul apelării functiei String::new
, o funcție care returnează o nouă instanță a unui String
. String
este un tip de string furnizat de librăria standard care este un text codificat UTF-8 care poate să crească.
Sintaxa ::
din linia ::new
indică că new
este o funcție asociată tipului String
. O funcție asociată este o funcție care este implementată pe un tip, în acest caz String
. Această funcție new
creează un string nou și gol. Vei găsi o funcție new
în multe tipuri deoarece este un nume obișnuit pentru o funcție care face o valoare nouă de un anume fel.
În întregime, linia let mut guess = String::new();
a creat o variabilă mutabilă care este în prezent legată de o nouă instanță goală a unui String
. Uf!
Primirea datelor introduse de utilizator
Ține minte că am inclus funcționalitatea de intrare/ieșire din biblioteca standard cu use std::io;
pe prima linie a programului. Acum vom apela funcția stdin
din modulul io
, care ne va permite să gestionăm datele introduse de utilizator:
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}");
}
Dacă nu am fi importat biblioteca io
cu use std::io;
la începutul programului, am putea totuși să folosim funcția scriind acest apel de funcție ca std::io::stdin
. Funcția stdin
returnează o instanță a std::io::Stdin
, care este un tip care reprezintă un handle la intrarea standard pentru terminalul tău.
Următoarea linie, .read_line(&mut guess)
apelează metoda read_line
pe handle-ul de intrare standard pentru a primi datele introduse de utilizator. De asemenea, trimitem &mut guess
ca argument pentru read_line
, pentru a-i spune în ce string să stocheze datele introduse de utilizator. Rolul principal al read_line
este de a prelua tot ceea ce tapează utilizatorul în intrarea standard și de a adăuga aceste date într-un string (fără a-i suprascrie conținutul), deci vom trimite acest string ca argument. String-ul de argument trebuie să fie mutabil pentru ca metoda să poată schimba conținutul lui.
Simbolul &
indică faptul că acest argument este o referință, care îți oferă o modalitate de a permite mai multor părți ale codului tău să acceseze o singură piesă de date fără a avea nevoie să copiezi acele date în memorie de mai multe ori. Referințele sunt o caracteristică complexă, iar unul dintre principalele avantaje ale Rust este cât de sigur și ușor e de utilizat referințele. Nu ai nevoie să știi o mulțime de acele detalii pentru a termina acest program. Deocamdată, tot ce trebuie să știi este că, la fel ca variabilele, referințele sunt imutabile în mod implicit. De aceea, trebuie să scrii &mut guess
în loc de &guess
pentru a-l face mutabil. (Capitolul 4 va explica referințele mai detaliat.)
Gestionarea potențialelor eșecuri cu Result
Încă lucrăm la această linie de cod. Acum discutăm despre a treia linie de text, dar reține că aceasta face încă parte dintr-o singură linie logică de cod. Partea următoare este această metodă:
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}");
}
Am fi putut scrie acest cod astfel:
io::stdin().read_line(&mut guess).expect("Failed to read line");
Cu toate acestea, o linie lungă este dificil de citit, deci este cel mai bine să o împărțim. Este adesea înțelept să introduci o linie nouă și alte spații albe pentru a ajuta la divizarea liniilor lungi atunci când apelezi o metodă cu sintaxa .nume_metoda()
. Acum să discutăm ce face această linie.
După cum am menționat mai devreme, read_line
introduce ceea ce introduce utilizatorul în string-ul pe care îl transmitem, dar returnează și o valoare de tip Result
. Result
este o enumerare, adesea numită și enum, care este un tip care poate fi într-una din multiple stări posibile. Fiecare stare posibilă o numim variantă.
Capitolul 6 va acoperi enumerările în mai mult detaliu. Scopul acestor tipuri Result
este de a codifica informațiile de gestionare a erorilor.
Variantele Result
sunt Ok
și Err
. Varianta Ok
indică faptul că operațiunea a fost reușită, iar în interiorul Ok
se află valoarea generată cu succes. Varianta Err
înseamnă că operațiunea a eșuat, iar Err
conține informații despre cum sau de ce a eșuat operațiunea.
Valorile de tip Result
, ca și valorile de orice alt tip, au metode definite asupra lor. O instanță a Result
are o metodă expect
pe care o poți apela. Dacă această instanță a Result
este o valoare Err
, expect
va provoca căderea programului și afișarea mesajului pe care l-ai transmis ca argument către expect
. Dacă metoda read_line
întoarce o Err
, ar fi probabil rezultatul unei erori provenite de la sistemul de operare de baza. Dacă această instanță a Result
este o valoare Ok
, expect
va prelua valoarea de return pe care Ok
o deține și îți va returna doar acea valoare pentru a o putea folosi. În acest caz, acea valoare este numărul de octeți în intrarea utilizatorului.
Dacă nu apelezi expect
, programul se va compila, dar vei primi un avertisment:
$ 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 avertizează că nu ai folosit valoarea Result
returnată de read_line
, indicând faptul că programul nu a gestionat o posibilă eroare.
Modul corect de a suprima avertismentul este de a scrie într-adevăr cod de gestionare a erorilor, dar în cazul nostru dorim doar să prăbușim acest program atunci când apare o problemă, așa că putem folosi expect
. Vei învăța despre recuperarea din erori în Capitolul 9.
Afișarea valorilor cu substituții println!
Afară de acolada de închidere, nu ne-a mai rămas decât un singur rând de discutat din codul de până acum:
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}");
}
Acest rând afișează șirul de caractere care conține acum intrarea utilizatorului. Setul de acolade {}
este un substituent: consideră {}
ca fiind niște clești de crab care țin o valoare pe loc. La afișarea valorii unei variabile, numele variabilei poate intra în acolade. La afișarea rezultatului evaluării unei expresii, plasați acoladele goale în string-ul de format, apoi urmați string-ul de format cu o listă separată prin virgulă cu expresii care trebuie afișate în fiecare substituent de acolade goale, în aceeași ordine. Afișarea unei variabile și a rezultatului unei expresii într-un singur apel către println!
ar arăta astfel:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
Acest cod ar afișa x = 5 and y + 2 = 12
.
Testarea primei părți
Să testăm prima parte a jocului de ghicit. Rulează-l folosind 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
Până în acest punct, prima parte a jocului este gata: noi primim input de la tastatură și apoi îl afișăm.
Generarea unui număr secret
În continuare, trebuie să generăm un număr secret pe care utilizatorul va încerca să îl ghicească. Numărul secret ar trebui să fie diferit de fiecare dată pentru ca jocul să fie distractiv de jucat de mai multe ori. Vom folosi un număr aleatoriu între 1 și 100 astfel încât jocul să nu fie prea dificil. Rust nu include încă funcționalitatea numerelor aleatorii în biblioteca sa standard. Cu toate acestea, echipa Rust oferă un rand
crate cu această funcționalitate.
Utilizarea unui crate pentru a obține mai multă funcționalitate
Este important să ținem minte că un crate este o colecție de fișiere sursă Rust. Proiectul nostru este un crate binar, adică un executabil. În contrast, crate-ul rand
este un crate de bibliotecă, conținând cod destinat să fie utilizat în cadrul altor programe și care nu poate fi executat de sine stătător.
Capacitatea lui Cargo de a coordona crate-uri externe este aspectul în care Cargo se evidențiază cu adevărat. Pentru a putea scrie cod ce folosește rand
, este necesar să facem o ajustare în fișierul Cargo.toml, incluzând crate-ul rand
între dependențe. Deschide fișierul chiar acum și adaugă la partea de jos următoarea linie, dedesubtul secțiunii [dependencies]
pe care Cargo a generat-o automat pentru tine. E vital să specifici rand
exact cum e indicat aici, cu această versiune, deoarece în caz contrar exemplele de cod din acest tutorial pot să nu funcționeze normal:
Numele fișierului: Cargo.toml
[dependencies]
rand = "0.8.5"
În fișierul Cargo.toml, tot ce se află după un antet aparține acelei secțiuni care continuă până când o altă secțiune începe. În [dependencies]
indicăm lui Cargo care crate-uri externe sunt necesare proiectului tău și ce versiuni ale acestor crate-uri trebuie utilizate. În acest caz, specificăm crate-ul rand
cu specificatorul de versiune semantică 0.8.5
. Cargo cunoaște Versionarea semantică (adesea numit SemVer), care e un standard pentru a scrie numerele de versiune. Specificantul 0.8.5
este de fapt o scurtătură pentru ^0.8.5
, ceea ce înseamnă orice versiune cel puțin 0.8.5 dar mai mică de 0.9.0.
Cargo consideră aceste versiuni ca având API-uri publice compatibile cu versiunea 0.8.5, iar această specificare garantează că vei obține cea mai recentă versiune cu patch-uri compatibile care vor compila corect cu codul din acest capitol. Nu este garantat ca versiunile 0.9.0 sau mai mari să păstreze același API ca exemplele utilizate aici.
Acum, fără a modifica vreun cod, să construim proiectul, așa cum este ilustrat în Listarea 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
Listarea 2-2: Afișajul rezultat după executarea cargo build
în urma adăugării crate-ului rand ca dependență
Poți întâlni numere de versiuni diferite (dar toate vor fi compatibile cu codul, datorită SemVer!) și linii diferite (care vor varia în funcție de sistemul de operare), iar ordinea liniilor poate fi diferită.
Atunci când introducem o dependență externă, Cargo caută și descarcă versiunile cele mai recente ale tuturor elementelor necesare acelei dependențe din registru, o copie a datelor de pe Crates.io. Crates.io este platforma unde comunitatea Rust își publică proiectele Rust open source, disponibile pentru folosirea de către toți.
Cargo verifică secțiunea [dependencies]
după ce actualizează registrul și descarcă crate-urile enumerate ce nu au fost încă descărcate. Deși am specificat doar rand
ca dependență, Cargo a descărcat și alte crate-uri de care rand
are nevoie pentru funcționarea sa. Odată ce crate-urile sunt descărcate, Rust le compilează, și apoi compilarea proiectului nostru are loc cu aceste dependențe incluse.
Dacă rulezi cargo build
din nou imediat, fără a modifica ceva, nu vei obține niciun afișaj în afară de linia Finished
. Cargo știe că a descărcat și compilat deja dependențele și că tu nu ai modificat nimic în fișierul Cargo.toml. De asemenea, Cargo înțelege că nici codul tău nu a fost schimbat, așadar nu recompilează nimic. Neavând ce să facă, se închide simplu.
Dacă ești în src/main.rs, faci o mică schimbare, salvezi și compilezi din nou, vei observa doar două linii de afișaj:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
Aceste linii arată că Cargo actualizează build-ul doar cu micuța ta modificare la fișierul src/main.rs. Dependențele tale nu s-au schimbat, așa că Cargo știe că poate reutiliza ceea ce deja a descărcat și compilat pentru acestea.
Asigurarea compilărilor reproductibile cu fișierul Cargo.lock
Cargo are un mecanism care garantează că poți reconstrui același artefact de fiecare dată când tu sau altcineva compilați codul: Cargo va utiliza doar versiunile specificate de dependențe, până nu decizi altceva. De exemplu, presupunem că săptămâna viitoare va fi lansată versiunea 0.8.6 a crate-ului rand
și că aceasta include o reparație critică de bug, dar totodată și o regresie care îți va afecta codul. Pentru a gestiona aceasta, Rust generează fișierul Cargo.lock prima oară când execuți cargo build
, deci acum îl avem în directoriul guessing_game.
La prima compilare a proiectului, Cargo determină toate versiunile compatibile pentru dependențe și le înscrie în fișierul Cargo.lock. La compilările ulterioare, Cargo va recunoaște prezența fișierului Cargo.lock și va folosi versiunile înregistrate acolo, evitând astfel munca repetitivă de selecție a versiunilor. Astfel, construcția proiectului tău devine reproductibilă automat. În alte cuvinte, proiectul va rămâne la versiunea 0.8.5 până când alegi explicit să faci o actualizare, datorită existenței fișierului Cargo.lock. Fiind esențial pentru reproduceri consecvente, fișierul Cargo.lock este adesea inclus în controlul de versiune alături de restul codului proiectului tău.
Actualizarea unui crate pentru a obține o versiune nouă
Atunci când dorești să actualizezi un crate, Cargo pune la dispoziție comanda update
, care va ignora fișierul Cargo.lock și va determina cele mai recente versiuni ce se potrivesc specificațiilor din Cargo.toml. Cargo va înregistra aceste versiuni în fișierul Cargo.lock. Implicit, Cargo caută versiuni care sunt mai mari decât 0.8.5 și mai mici de 0.9.0. Dacă crate-ul rand
a introdus două noi versiuni, 0.8.6 și 0.9.0, ai vedea următorul rezultat dacă ai executa cargo update
:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo va ignora lansarea versiunii 0.9.0. De asemenea, ai remarca o modificare în fișierul tău Cargo.lock, care arată că acum folosești versiunea 0.8.6 a crate-ului rand
. Pentru a utiliza versiunea rand
0.9.0 sau orice altă versiune din seria 0.9.x, trebuie să actualizezi fisierul Cargo.toml astfel:
[dependencies]
rand = "0.9.0"
Data următoare când rulezi cargo build
, Cargo va actualiza lista de crate-uri disponibile și va reevalua cerința ta pentru rand
în funcție de noua versiune pe care ai specificat-o.
Există multe alte lucruri de spus despre Cargo și ecosistemul său, pe care le vom discuta în Capitolul 14. Până acum, aceste informații sunt tot ce trebuie să știi. Cargo facilitează în mare măsură reutilizarea bibliotecilor, ceea ce permite programatorilor Rust să creeze proiecte mai compacte, compuse din diverse pachete.
Generarea unui număr aleatoriu
Să începem folosirea bibliotecii rand
pentru a genera un număr de ghicit. Următorul pas este să actualizăm fișierul src/main.rs, așa cum se arată în Listarea 2-3.
Numele fișierului: src/main.rs
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}");
}
Listarea 2-3: Adăugarea de cod pentru a genera un număr aleatoriu
În primul rând, adăugăm linia use rand::Rng;
. Trăsătura Rng
definește metodele pe care le implementează generatorii de numere aleatorii și trebuie să fie în domeniul de vizibilitate pentru a folosi acele metode. Trăsăturile vor fi explicate în detaliu în Capitolul 10.
Următoarea parte implică adăugarea a două linii noi în cod. În prima linie apelăm funcția rand::thread_rng
, care ne oferă generatorul de numere aleatorii specific firului de execuție curent și care este inițiat de sistemul de operare. Apoi utilizăm metoda gen_range
de la acel generator de numere. Metoda gen_range
, definită de trăsătura Rng
adusă în context cu instrucțiunea use rand::Rng;
, primește o expresie de diapazon și generează un număr aleatoriu în interiorul acelui diapazon. Expresia de diapazon pe care o utilizăm este start..=end
, care este inclusivă pentru ambele limite, deci specificăm 1..=100
pentru a obține un număr între 1 și 100.
Notă: Nu vei cunoaște întotdeauna care trăsături trebuie utilizate și ce metode și funcții să apelezi de la un crate, prin urmare, fiecare crate vine echipat cu propria documentație și instrucțiuni pentru a te ghida în utilizarea sa. O funcționalitate practică a Cargo este că executarea comenzii
cargo doc --open
va genera documentația furnizată de toate dependențele tale local și o va deschide în navigatorul web. Dacă ai curiozitatea de a explora alte funcții ale crate-uluirand
, de exemplu, executăcargo doc --open
și selecteazărand
din bara laterală din partea stângă.
Cea de-a doua linie nouă afișează numărul secret, ceea ce este util pe durata dezvoltării programului pentru a-l putea testa, însă o vom șterge în versiunea finală. Nu se poate spune că este un joc prea incitant dacă programul dezvăluie soluția imediat ce este lansat.
Încearcă să rulezi programul de câteva ori:
$ 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
Ar trebui să obții numere aleatorii diferite, iar toate ar trebui să fie numere între 1 și 100. Excelentă treabă!
Compararea ghicirii cu numărul secret
Acum că avem o intrare de la utilizator și un număr aleator, le putem compara. Acest pas este arătat în Listarea 2-4. Reține că acest cod nu se va compila încă, așa cum vom explica.
Numele fișierului: src/main.rs
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!"),
}
}
Listarea 2-4: Gestionarea posibilelor rezultate ale comparării a două numere
Mai întâi, adăugăm o altă declarație use
, introducând în scopul nostru un tip numit std::cmp::Ordering
din biblioteca standard. Tipul Ordering
este o altă enumerare și are variantele Less
, Greater
și Equal
. Acestea sunt cele trei rezultate posibile când compari două valori.
Apoi, adăugăm cinci linii noi la final care utilizează tipul Ordering
. Metoda cmp
compară două valori și poate fi apelată pe orice tip de valori comprabile. E primește o referință la ce se dorește de a fi comparat: aici se face compararea între guess
și secret_number
. Apoi returnează o variantă a enumerației Ordering
pe care am adus-o în scop cu declarația use
. Folosim o expresie match
pentru a decide ce să facem în continuare bazat pe ce variantă a Ordering
a fost returnată de apelul la cmp
cu valorile din guess
și secret_number
.
O expresie match
este compusă din brațe. Un braț constă dintr-un model (numit și pattern) pentru care se face potrivirea, și din codul care ar trebui să ruleze dacă valoarea dată lui match
se potrivește cu modelul acelui braț. Rust ia valoarea dată lui match
și o compară cu modelul fiecărui braț pe rând. Modelele și constructul match
reprezintă caracteristici forte ale lui Rust: îți permit să exprimi o varietate de situații pe care codul tău le-ar putea întâlni și se asigură că le gestionezi pe toate. Aceste caracteristici vor fi acoperite în detaliu în Capitolul 6 și Capitolul 18, respectiv.
Acum să trecem printr-un exemplu cu expresia match
pe care o folosim aici. Să presupunem că utilizatorul a ghicit numărul 50 și de data aceasta numărul secret generat aleator este 38.
Atunci când codul compară numărul 50 cu 38, metoda cmp
va returna Ordering::Greater
, deoarece 50 este mai mare decât 38. Expresia match
primește valoarea Ordering::Greater
și începe să verifice modelul pentru fiecare braț. Analizează primul tipar de braț, Ordering::Less
, și vede că valoarea Ordering::Greater
nu se potrivește cu Ordering::Less
, deci ignoră codul din acel braț și trece la brațul următor. Modelul următorului braț este Ordering::Greater
, care se potrivește cu Ordering::Greater
! Codul asociat acestui braț va fi executat și va imrima pe ecran Too big!
. Expresia match
se încheie după prima potrivire reușită, deci nu se va uita la ultimul braț în acest scenariu.
Totuși, codul din Listarea 2-4 încă nu se va compila. Să încercăm:
$ 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
Esența erorii indică existența unor tipuri incompatibile. Rust se bazează pe un sistem de tipuri puternic și tipizat static. În același timp, Rust suportă inferență de tipuri. Atunci când am scris let mut guess = String::new()
, Rust a inferat că guess
ar trebui să fie un String
fără să fie necesar să specificăm tipul. Pe de altă parte, secret_number
este de un tip numeric. Există mai multe tipuri numerice în Rust care pot avea valori între 1 și 100: i32
, un număr pe 32 de biți; u32
, un număr nesemnat pe 32 de biți; i64
, un număr pe 64 de biți; printre altele. Dacă nu se indică altfel, Rust alege i32
în mod implicit, care este tipul pentru secret_number
dacă nu adăugăm informații despre tip în altă parte care ar determina Rust să infereze un alt tip numeric. Eroarea apare pentru că Rust nu poate face comparație între un string și un tip numeric.
Pentru a o rezolva, intenționăm să convertim String
-ul primit ca input într-un tip numeric real, astfel încât să putem face comparația numerică cu numărul secret. Acest lucru se realizează adăugând următoarea linie în corpul funcției main
:
Numele fișierului: 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!"),
}
}
Linia este:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
Creăm o variabilă numită guess
. Dar așteaptă, nu există deja o variabilă cu numele guess
în program? Da, dar, convenabil, Rust ne permite să umbrim precedentul guess
cu unul nou. Umbrirea ne permite să reutilizăm numele variabilei guess
în loc să fim obligați să creăm două variabile diferite, cum ar fi guess_str
și guess
, de exemplu. Vom detalia acest concept în Capitolul 3, dar pentru moment, să știi că această funcționalitate este adesea folosită când vrei să convertești o valoare dintr-un tip în altul.
Noua variabilă este asociată expresiei guess.trim().parse()
. Partea guess
din cadrul expresiei se referă la variabila originală guess
care conținea input-ul ca un string. Metoda trim
pe o instanță String
va elimina orice spațiu alb (whitespace) de la început și sfârșitul string-ului, lucru necesar pentru a putea compara string-ul cu un u32
, ce poate conține doar date numerice. Utilizatorul trebuie să apese enter pentru a completa read_line
și așa să introducă ghicirea sa, adăugând prin asta un caracter de linie nouă la string. De exemplu, dacă utilizatorul scrie 5 și apasă enter, guess
arată așa: 5\n
. \n
reprezintă „newline” (linie nouă). (Pe Windows, apăsarea enter aduce un carriage return și un newline, \r\n
.) Metoda trim
înlătură \n
sau \r\n
, lăsând doar 5
.
Metoda parse
de pe string-uri transformă un string într-un alt tip. În acest caz, o utilizăm pentru a converti un string într-un număr. Trebuie să specificăm în Rust tipul exact de număr pe care îl dorim, utilizând let guess: u32
. Două puncte (:
) care urmează după guess
indică faptul că vom adnota tipul variabilei. Rust include diferite tipuri de numere; u32
, pe care îl vedem aici, reprezintă un întreg pozitiv fără semn și de 32 de biți. Este o opțiune predilectă pentru reprezentarea unui număr mic și pozitiv. Despre alte tipuri de numere vei afla în Capitolul 3.
De asemenea, prin folosirea adnotării u32
în acest exemplu și prin comparația cu secret_number
, Rust va înțelege că și secret_number
trebuie să fie u32
. Astfel, comparația se va face între două valori de același tip!
Metoda parse
este aplicabilă doar caracterelor ce pot fi convertite în mod logic către numere și, prin urmare, este susceptibilă de erori. Dacă, spre exemplu, string-ul ar conţine A👍%
, nu ar exista nicio modalitate de a-l converti într-un număr. Fiind posibil să eșueze, metoda parse
returnează un tip Result
, într-un mod similar cu metoda read_line
(abordată anterior în „Tratarea eșecurilor potențiale cu Result
”). Vom reacționa în fața acestui Result
în același mod, utilizând din nou metoda expect
. Dacă parse
întoarce o variantă de Err
a tipului Result
, deoarece nu a putut transforma string-ul într-un număr, apelul expect
va opri jocul și va tipări mesajul specificat de noi. În situația în care parse
reușește să convertească string-ul într-un număr cu succes, va returna varianta Ok
a Result
, iar metoda expect
va extrage numărul dorit din valoarea Ok
.
Să rulăm acum programul:
$ 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!
Foarte bine! Deși au fost adăugate spații înainte de ghicire, programul a reușit totuși să-și dea seama că utilizatorul a ghicit numărul 76. Rulează programul de câteva ori pentru a verifica comportamentul diferit cu diferite tipuri de input: ghicește numărul corect, ghicește un număr care este prea mare sau ghicește un număr care este prea mic.
Acum avem majoritatea jocului funcțional, dar utilizatorul poate face doar o singură ghicire. Să schimbăm asta adăugând o buclă!
Permiterea multiplelor ghiciri cu bucle
Cuvântul cheie loop
creează o buclă infinită. Vom adăuga o buclă pentru a oferi utilizatorilor mai multe încercări de a ghici numărul:
Numele fișierului: 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!"),
}
}
}
După cum se poate observa, am mutat tot de la promptul de introducere a ghicirii într-o buclă. Asigură-te că liniile din interiorul buclei sunt indentate cu patru spații suplimentare fiecare și rulează din nou programul. Acum programul va solicita o nouă ghicire la nesfârșit, ceea ce introduce efectiv o nouă problemă: pare că utilizatorul nu are posibilitatea să părăsească programul!
Utilizatorul poate întrerupe întotdeauna programul utilizând combinația de taste ctrl-c. Există însă și o altă cale de a se elibera de acest ciclu infinit, așa cum am menționat în discuția despre parse
din secțiunea “Compararea ghicirii cu numărul secret”: dacă utilizatorul scrie ceva ce nu este un număr, atunci programul va da eroare și se va opri. Putem folosi acest comportament ca o metodă prin care utilizatorul poate opri programul, după cum urmează:
$ 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.
adio
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
Dacă introduci adio
, jocul se va opri, însă vei observa că se va întâmpla același lucru dacă introduci orice alt input care nu e un număr. Aceasta este departe de a fi ideal; ne dorim ca jocul să se încheie și atunci când numărul ghicit este cel corect.
Terminarea jocului la o ghicire corectă
Să programăm jocul să se termine atunci când utilizatorul câștigă, prin adăugarea unei instrucțiuni break
:
Numele fișierului: 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;
}
}
}
}
Introducerea liniei break
imediat după You win!
determină programul să părăsească bucla atunci când utilizatorul ghicește numărul secret în mod corect. Părăsirea buclei înseamnă de asemenea finalizarea programului, deoarece bucla reprezintă ultima secțiune a funcției main
.
Procesarea intrărilor non-valide
Pentru a îmbunătăți comportamentul jocului, în loc de a opri programul când utilizatorul introduce ceva ce nu este un număr, putem configura jocul să ignore acest tip de input, permițând utilizatorului să continue să ghicească. Acest lucru poate fi realizat modificând linia în care valoarea guess
este convertită din String
în u32
, așa cum este ilustrat în Listarea 2-5.
Numele fișierului: 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();
// --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;
}
}
}
}
Listarea 2-5: Ignorarea unui input care nu este un număr și solicitarea unei noi încercări în locul oprii programului
Trecem de la o funcție expect
la o expresie match
pentru a gestiona erorile în locul oprii programului. Este important să ne amintim că parse
returnează un tip Result
și că Result
este o enumerare cu variantele Ok
și Err
. Folosim o expresie match
, la fel ca atunci când lucrăm cu Ordering
rezultatul metodei cmp
.
Când parse
poate să convertească în mod eficient string-ul într-un număr, ne întoarce o valoare Ok
care include numărul rezultat. Această valoare Ok
se va potrivi cu modelul din primul caz al expresiei match
, iar ca rezultat, vom primi numărul creat de parse
și inclus în Ok
. Numărul va fi pus exact unde trebuie în noua variabilă guess
pe care o definim.
Dacă parse
nu poate să convertească string-ul într-un număr, ne va da o valoare Err
care deține detalii despre eroare. Valoarea Err
nu se potrivește cu modelul Ok(num)
din primul caz al match
, dar se potrivește cu Err(_)
din cel de-al doilea caz. Folosind _
ca wildcard, spunem că dorim să captăm orice fel de valoare Err
, fără a ne preocupa de conținutul acesteia. Programul va rula codul corespunzător celui de-al doilea caz, adică continue
, care comandă programului să treacă la următoarea iterație a buclei loop
și să solicite o încercare nouă. Astfel, ignorăm orice posibilă eroare care ar putea apărea în timpul funcției parse
.
Acum totul în program ar trebui să funcționeze conform așteptărilor. Să încercăm:
$ 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!
Minunat! Cu o mică ultimă ajustare, vom termina jocul de ghicit. Amintește-ți că programul încă afișează numărul secret. Acest lucru a funcționat bine pentru testare, dar strică jocul. Să ștergem println!
care afișează numărul secret. Listarea 2-6 arată codul final.
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);
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;
}
}
}
}
Listarea 2-6: Codul complet al jocului de ghicire
La acest moment, ai reușit să construiești cu succes jocul de ghicire. Felicitări!
Sumar
Acest proiect a fost o modalitate practică de a face cunoștință cu multe concepte noi din Rust: let
, match
, funcții, utilizarea crate-urilor externe, și multe altele. În următoarele câteva capitole, vei învăța despre aceste concepte în mai multe detalii. Capitolul 3 acoperă funcționalități pe care majoritatea limbajelor de programare le au, cum ar fi variabile, tipuri de date și funcții, și arată cum să le folosești în Rust. Capitolul 4 explorează posesiunea (ownership), o caracteristică ce distinge Rust de alte limbaje. Capitolul 5 discută despre structuri și sintaxa metodelor, iar Capitolul 6 explică cum funcționează enumerările.