Arvauspelin ohjelmointi
Hypätään suoraan Rustin pariin työstämällä yhdessä käytännön projekti! Tämä
luku esittelee sinulle muutamia yleisiä Rust-käsitteitä näyttämällä, miten
voit käyttää niitä oikeassa ohjelmassa. Opit let-määrittelyistä, match-lausekkeista, metodeista, assosioiduista
funktioista, ulkoisista crateista ja paljon muusta! Seuraavissa luvuissa perehdymme
näihin ideoihin tarkemmin. Tässä luvussa harjoittelet vain
perusasioita.
Toteutamme klassisen aloittelijatason ohjelmointitehtävän: arvauspelin. Näin se toimii: ohjelma generoi satunnaisen kokonaisluvun väliltä 1–100. Se pyytää sitten pelaajaa syöttämään arvauksen. Kun arvaus on annettu, ohjelma kertoo, onko arvaus liian pieni vai liian suuri. Jos arvaus on oikein, peli tulostaa onnitteluviestin ja sulkeutuu.
Uuden projektin luominen
Luo uusi projekti siirtymällä projects-hakemistoon, jonka loit luvussa 1, ja luo uusi projekti käyttäen Cargoa näin:
$ cargo new guessing_game
$ cd guessing_game
Ensimmäinen komento cargo new ottaa projektin nimen (guessing_game)
ensimmäisenä argumenttina. Toinen komento siirtyy uuden projektin
hakemistoon.
Tarkastele luotua Cargo.toml-tiedostoa:
Filename: Cargo.toml
{{#include ../listings/ch02-guessing-game-tutorial/no-listing-01-cargo-new/Cargo.toml}}
Kuten näit luvussa 1, cargo new generoi sinulle ”Hello, world!” -ohjelman.
Tarkista src/main.rs-tiedosto:
Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-01-cargo-new/src/main.rs}}
}
Käännetään ja ajetaan nyt tämä ”Hello, world!” -ohjelma samassa vaiheessa
käyttämällä cargo run -komentoa:
{{#include ../listings/ch02-guessing-game-tutorial/no-listing-01-cargo-new/output.txt}}
run-komento on kätevä, kun täytyy nopeasti iteroida projektia,
kuten teemme tässä pelissä, testaten nopeasti jokaisen version ennen siirtymistä
seuraavaan.
Avaa src/main.rs-tiedosto uudelleen. Kirjoitat kaiken koodin tähän tiedostoon.
Arvauksen käsittely
Arvauspeli-ohjelman ensimmäinen osa pyytää käyttäjän syötettä, käsittelee sen ja tarkistaa, että syöte on odotetussa muodossa. Aloitamme sallimalla pelaajan syöttää arvauksen. Syötä listauksen 2-1 koodi tiedostoon src/main.rs.
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:all}}
Tämä koodi sisältää paljon tietoa, joten käydään se läpi rivi riviltä. Saadaksemme
käyttäjän syötteen ja tulostaaksemme tuloksen, meidän täytyy tuoda
io-kirjasto näkyviin. io-kirjasto tulee standardikirjastosta,
joka tunnetaan nimellä std:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:io}}
Oletuksena Rustilla on joukko kohteita, jotka on määritelty standardikirjastossa ja jotka se tuo näkyviin jokaisessa ohjelmassa. Tätä joukkoa kutsutaan preludiksi, ja voit nähdä kaiken siinä standardikirjaston dokumentaatiossa.
Jos haluamasi tyyppi ei ole preludissa, sinun täytyy tuoda kyseinen tyyppi
näkyviin eksplisiittisesti use-lauseella. std::io-kirjaston käyttö
tarjoaa useita hyödyllisiä ominaisuuksia, mukaan lukien mahdollisuuden hyväksyä
käyttäjän syöte.
Kuten näit luvussa 1, main-funktio on ohjelman sisääntulopiste:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:main}}
fn-syntaksi julistaa uuden funktion; sulkeet () osoittavat, ettei
ole parametreja; ja aaltosulje { aloittaa funktion rungon.
Kuten opit myös luvussa 1, println! on makro, joka tulostaa merkkijonon
näytölle:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print}}
Tämä koodi tulostaa kehotteen, joka kertoo mistä pelissä on kyse, ja pyytää syötettä käyttäjältä.
Arvojen tallentaminen muuttujilla
Seuraavaksi luomme muuttujan tallentaaksemme käyttäjän syötteen, näin:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:string}}
Nyt ohjelma alkaa olla mielenkiintoinen! Tässä lyhyessä
rivissä tapahtuu paljon. Käytämme let-lauseketta luodaksemme muuttujan. Tässä toinen esimerkki:
let apples = 5;
Tämä rivi luo uuden muuttujan nimeltä apples ja sitoo sen arvoon 5. Rustissa
muuttujat ovat oletuksena muuttumattomia, eli kun annamme muuttujalle
arvon, arvo ei muutu. Käsittelemme tätä käsitettä tarkemmin
”Muuttujat ja muutettavuus”
-osiossa luvussa 3. Tehdäksemme muuttujan muutettavaksi lisäämme mut-avainsanan ennen
muuttujan nimeä:
let apples = 5; // immutable
let mut bananas = 5; // mutable
Huom:
//-syntaksi aloittaa kommentin, joka jatkuu rivin loppuun. Rust jättää huomiotta kaiken kommenteissa. Käsittelemme kommentteja tarkemmin luvussa 3.
Palatessa arvauspeli-ohjelmaan, tiedät nyt että let mut guess
esittelee muutettavan muuttujan nimeltä guess. Yhtäsuuruusmerkki (=) kertoo Rustille, että
haluamme sitoa jotain muuttujaan nyt. Yhtäsuuruusmerkin oikealla puolella on
arvo, johon guess sidotaan, eli String::new-funktion kutsumisen tulos,
funktio joka palauttaa uuden String-instanssin.
String on standardikirjaston tarjoama merkkijonotyyppi,
joka on kasvava, UTF-8 -koodattu tekstinpätkä.
::-syntaksi ::new-rivillä osoittaa, että new on assosioitu
funktio String-tyypille. Assosioitu funktio on funktio, joka on
toteutettu tyypille, tässä tapauksessa String:lle. Tämä new-funktio luo
uuden, tyhjän merkkijonon. Löydät new-funktion monilta tyypeiltä, koska se on
yleinen nimi funktiolle, joka luo jonkinlaisen uuden arvon.
Kokonaisuudessaan let mut guess = String::new(); -rivi on luonut muutettavan
muuttujan, joka on tällä hetkellä sidottu uuteen, tyhjään String-instanssiin. Huh!
Käyttäjän syötteen vastaanottaminen
Muista, että sisällytimme syöte-/tulostustoiminnallisuuden standardikirjastosta
use std::io; -lauseella ohjelman ensimmäisellä rivillä. Kutsumme nyt
stdin-funktiota io-moduulista, mikä antaa meidän käsitellä käyttäjän
syötettä:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:read}}
Jos emme olisi tuoneet io-kirjastoa näkyviin use std::io; -lauseella ohjelman
alussa, voisimme silti käyttää funktiota kirjoittamalla funktiokutsun muodossa
std::io::stdin. stdin-funktio palauttaa instanssin
std::io::Stdin -tyypistä, joka edustaa
käsittelijää päätteen standardisyötteelle.
Seuraavaksi rivi .read_line(&mut guess) kutsuu read_line -metodia standardisyötteen käsittelijällä saadakseen syötteen käyttäjältä.
Välitämme myös &mut guess argumenttina read_line-metodille kertoaksemme sille, mihin
merkkijonoon tallentaa käyttäjän syötteen. read_line-metodin tehtävä on ottaa
mitä tahansa käyttäjä kirjoittaa standardisyötteeseen ja liittää se merkkijonoon
(ylikirjoittamatta sen sisältöä), joten välitämme tuon merkkijonon
argumenttina. Merkkijonoargumentin täytyy olla muutettava, jotta metodi voi muuttaa
merkkijonon sisältöä.
& osoittaa, että tämä argumentti on viittaus, joka antaa tavan
antaa useiden koodin osien käyttää samaa dataa ilman, että dataa täytyy
kopioida muistiin useita kertoja. Viittaukset ovat monimutkainen ominaisuus,
ja yksi Rustin suurista eduista on, kuinka turvallista ja helppoa viittausten
käyttö on. Et tarvitse tietää paljoa näistä yksityiskohdista tämän
ohjelman viimeistelyyn. Toistaiseksi sinun tarvitsee tietää vain, että kuten muuttujat, viittaukset ovat
oletuksena muuttumattomia. Siksi sinun täytyy kirjoittaa &mut guess eikä
&guess tehdäksesi sen muutettavaksi. (Luku 4 selittää viittaukset tarkemmin.)
Mahdollisen epäonnistumisen käsittely Result-tyypillä
Työstämme vielä tätä koodiriviä. Käsittelemme nyt kolmatta tekstiriviä, mutta huomaa, että se on edelleen osa yhtä loogista koodiriviä. Seuraava osa on tämä metodi:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:expect}}
Olisimme voineet kirjoittaa tämän koodin näin:
io::stdin().read_line(&mut guess).expect("Failed to read line");
Yksi pitkä rivi on kuitenkin vaikea lukea, joten on parasta jakaa se. On
usein viisasta lisätä rivinvaihto ja muuta välilyöntiä auttamaan jakamaan pitkiä
rivejä, kun kutsut metodia .method_name() -syntaksilla. Käsitellään nyt,
mitä tämä rivi tekee.
Kuten aiemmin mainittiin, read_line laittaa mitä tahansa käyttäjä syöttää merkkijonoon,
jonka välitämme sille, mutta se palauttaa myös Result-arvon. Result on enum, usein kutsuttu enumiksi,
joka on tyyppi, joka voi olla yhdessä useista mahdollisista tiloista. Kutakin
mahdollista tilaa kutsutaan variantiksi.
Luku 6 käsittelee enumeja tarkemmin. Näiden
Result-tyyppien tarkoitus on koodata virheenkäsittelytietoa.
Result-tyypin variantit ovat Ok ja Err. Ok-variantti osoittaa, että
operaatio onnistui, ja se sisältää onnistuneesti generoidun arvon.
Err-variantti tarkoittaa, että operaatio epäonnistui, ja se sisältää tietoa
siitä, miten tai miksi operaatio epäonnistui.
Result-tyypin arvoilla, kuten minkä tahansa tyypin arvoilla, on niille määriteltyjä metodeja.
Result-instanssilla on expect-metodi,
jota voit kutsua. Jos tämä Result-instanssi on Err-arvo, expect
saa ohjelman kaatumaan ja näyttämään viestin, jonka välitit
argumenttina expect-metodille. Jos read_line-metodi palauttaa Err-arvon, se olisi
todennäköisesti käyttöjärjestelmän virheen seurausta.
Jos tämä Result-instanssi on Ok-arvo, expect ottaa palautusarvon,
jonka Ok pitää sisällään, ja palauttaa sen sinulle, jotta voit käyttää sitä.
Tässä tapauksessa tuo arvo on käyttäjän syötteen tavumäärä.
Jos et kutsu expect-metodia, ohjelma kääntyy, mutta saat varoituksen:
{{#include ../listings/ch02-guessing-game-tutorial/no-listing-02-without-expect/output.txt}}
Rust varoittaa, ettet ole käyttänyt read_line-metodin palauttamaa Result-arvoa,
osoittaen, ettei ohjelma ole käsitellyt mahdollista virhettä.
Oikea tapa hiljentää varoitus on todella kirjoittaa virheenkäsittelykoodi,
mutta tapauksessamme haluamme vain kaataa ohjelman, kun ongelma ilmenee, joten voimme
käyttää expect-metodia. Opit virheistä toipumisesta luvussa
9.
Arvojen tulostaminen println!-paikkamerkeillä
Sulkevan aaltosulkeen lisäksi on vielä yksi rivi käsiteltävänä tähänastisessa koodissa:
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print_guess}}
Tämä rivi tulostaa merkkijonon, joka sisältää nyt käyttäjän syötteen. {}-merkkien
joukko on paikkamerkki: ajattele {}:tä pieninä rapupihdeinä, jotka pitävät
arvon paikallaan. Kun tulostat muuttujan arvon, muuttujan nimi voi
mennä aaltosulkeiden sisään. Kun tulostat lausekkeen evaluoinnin tuloksen, laita tyhjät aaltosulkeet
muotoilumerkkijonoon ja lisää sen jälkeen pilkulla erotettu lista lausekkeista,
jotka tulostetaan kuhunkin tyhjään
aaltosuljepaikkamerkkiin samassa järjestyksessä. Muuttujan ja lausekkeen tulostaminen
yhdessä println!-kutsussa näyttäisi tältä:
#![allow(unused)]
fn main() {
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
}
Tämä koodi tulostaisi x = 5 and y + 2 = 12.
Ensimmäisen osan testaaminen
Testataan arvauspelin ensimmäistä osaa. Aja se käyttämällä cargo run-komentoa:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
Tässä vaiheessa pelin ensimmäinen osa on valmis: saamme syötteen näppäimistöltä ja tulostamme sen.
Salaisen luvun generointi
Seuraavaksi meidän täytyy generoida salainen luku, jota käyttäjä yrittää arvata. Salaisen
luvun pitäisi olla eri joka kerta, jotta peli on hauska pelata useammin
kuin kerran. Käytämme satunnaista lukua väliltä 1–100, jotta peli ei ole liian
vaikea. Rust ei vielä sisällytä satunnaislukutoiminnallisuutta standardikirjastoonsa.
Rust-tiimi tarjoaa kuitenkin rand-craten, jossa
kyseinen toiminnallisuus on.
Craten käyttö lisätoiminnallisuuden saamiseksi
Muista, että crate on kokoelma Rust-lähdekooditiedostoja. Projekti,
jota olemme rakentaneet, on binaaricrate, joka on suoritettava ohjelma. rand-
crate on kirjastocrate, joka sisältää koodia, joka on tarkoitettu käytettäväksi
muissa ohjelmissa eikä sitä voi suorittaa itsenäisesti.
Cargon koordinointi ulkoisista crateista on se, missä Cargo todella loistaa. Ennen kuin
voimme kirjoittaa koodia, joka käyttää rand-cratea, meidän täytyy muokata Cargo.toml-tiedostoa
sisällyttääksemme rand-craten riippuvuudeksi. Avaa tiedosto nyt ja lisää
seuraava rivi pohjaan, [dependencies]-osion otsikon alle, jonka
Cargo loi sinulle. Varmista, että määrittelet rand:in täsmälleen kuten tässä,
tällä versionumerolla, tai tämän opetusohjelman koodiesimerkit eivät välttämättä toimi:
Filename: Cargo.toml
{{#include ../listings/ch02-guessing-game-tutorial/listing-02-02/Cargo.toml:8:}}
Cargo.toml-tiedostossa kaikki otsikon jälkeen tuleva kuuluu siihen
osioon, joka jatkuu, kunnes toinen osio alkaa. [dependencies]-osiossa
kerrot Cargolle, mistä ulkoisista crateista projektisi riippuu ja mitä versioita
näistä crateista tarvitset. Tässä tapauksessa määrittelemme rand-craten
semanttisella versionumerolla 0.10.1. Cargo ymmärtää semanttisen
versionhallinnan (joskus kutsutaan SemVeriksi), joka on
standardi versionumeroiden kirjoittamiseen. Määrite 0.10.1 on itse asiassa
lyhennys muodolle ^0.10.1, mikä tarkoittaa mitä tahansa versiota, joka on vähintään 0.10.1 mutta
alle 0.11.0.
Cargo pitää näitä versioita julkisesti yhteensopivina version 0.10.1 API:n kanssa, ja tämä määrite varmistaa, että saat uusimman korjausjulkaisun, joka vielä kääntyy tämän luvun koodin kanssa. Mikään versio 0.11.0 tai suurempi ei ole taattu sisältävän samaa API:a kuin seuraavat esimerkit käyttävät.
Nyt, muuttamatta mitään koodia, käännetään projekti, kuten listauksessa 2-2.
$ cargo build
Updating crates.io index
Locking 8 packages to latest Rust 1.96.0 compatible versions
Downloaded rand_core v0.10.1
Downloaded chacha20 v0.10.1
Downloaded rand v0.10.1
Downloaded 3 crates (162.9KiB) in 0.59s
Compiling libc v0.2.186
Compiling rand_core v0.10.1
Compiling getrandom v0.4.3
Compiling cfg-if v1.0.4
Compiling chacha20 v0.10.1
Compiling rand v0.10.1
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.03s
cargo build -komennon ajosta rand-craten lisäämisen jälkeen riippuvuudeksiSaatat nähdä eri versionumerot (mutta ne ovat kaikki yhteensopivia koodin kanssa SemVerin ansiosta!) ja eri rivejä (käyttöjärjestelmästä riippuen), ja rivit voivat olla eri järjestyksessä.
Kun sisällytämme ulkoisen riippuvuuden, Cargo hakee uusimmat versiot kaikesta, mitä kyseinen riippuvuus tarvitsee, rekisteristä, joka on kopio dataa Crates.io-sivustolta. Crates.io on paikka, jossa Rust-ekosysteemin ihmiset julkaisevat avoimen lähdekoodin Rust-projektejaan muiden käytettäväksi.
Rekisterin päivityksen jälkeen Cargo tarkistaa [dependencies]-osion ja
lataa kaikki listatut cratet, joita ei ole vielä ladattu. Tässä tapauksessa,
vaikka listasimme vain rand:in riippuvuudeksi, Cargo haki myös muita crateja,
joista rand riippuu toimiakseen. Cratet ladattuaan Rust kääntää
ne ja kääntää sitten projektin riippuvuuksien ollessa käytettävissä.
Jos ajat heti cargo build-komennon uudelleen tekemättä muutoksia, et
saa mitään tulostetta Finished-rivin lisäksi. Cargo tietää, että se on jo
lataanut ja kääntänyt riippuvuudet, etkä ole muuttanut mitään
niistä Cargo.toml-tiedostossasi. Cargo tietää myös, ettet ole muuttanut
mitään koodissasi, joten se ei käännä sitäkään uudelleen. Kun ei ole mitään
tehtävää, se yksinkertaisesti lopettaa.
Jos avaat src/main.rs-tiedoston, teet pienen muutoksen, tallennat sen ja käännät uudelleen, näet vain kaksi riviä tulostetta:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Nämä rivit osoittavat, että Cargo päivittää käännöksen vain pienellä muutoksellasi src/main.rs-tiedostoon. Riippuvuutesi eivät ole muuttuneet, joten Cargo tietää voivansa käyttää uudelleen jo ladattua ja kääntämäänsä.
Toistettavien käännösten varmistaminen Cargo.lock-tiedostolla
Cargolla on mekanismi, joka varmistaa, että voit kääntää saman artefaktin joka kerta,
kun sinä tai kuka tahansa kääntää koodisi: Cargo käyttää vain niitä riippuvuuksien versioita,
joita määritit, kunnes ilmoitat toisin. Sanotaan esimerkiksi, että
ensi viikolla rand-craten versio 0.10.2 julkaistaan, ja tuo versio
sisältää tärkeän bugikorjauksen, mutta myös regressiota, joka
rikkoo koodisi. Tätä varten Rust luo Cargo.lock-tiedoston ensimmäisellä
cargo build -ajolla, joten meillä on nyt tämä guessing_game-
hakemistossa.
Kun käännät projektin ensimmäistä kertaa, Cargo selvittää kaikki riippuvuuksien versiot, jotka sopivat kriteereihin, ja kirjoittaa ne sitten Cargo.lock-tiedostoon. Kun käännät projektisi tulevaisuudessa, Cargo näkee Cargo.lock-tiedoston olevan olemassa ja käyttää siellä määriteltyjä versioita sen sijaan, että tekisi kaiken työn versioiden selvittämiseksi uudelleen. Tämä antaa sinulle toistettavan käännöksen automaattisesti. Toisin sanoen projektisi pysyy versiossa 0.10.1, kunnes päivität eksplisiittisesti, Cargo.lock-tiedoston ansiosta. Koska Cargo.lock-tiedosto on tärkeä toistettaville käännöksille, se tarkistetaan usein versionhallintaan muun projektikoodin kanssa.
Craten päivittäminen uuden version saamiseksi
Kun haluat päivittää craten, Cargo tarjoaa update-komennon,
joka ohittaa Cargo.lock-tiedoston ja selvittää kaikki uusimmat versiot,
jotka sopivat määrittelyihisi Cargo.toml-tiedostossa. Cargo kirjoittaa sitten nämä
versiot Cargo.lock-tiedostoon. Muuten Cargo etsii oletuksena vain
versioita, jotka ovat suurempia kuin 0.10.1 ja pienempiä kuin 0.11.0. Jos rand-cratella on
julkaistu kaksi uutta versiota 0.10.2 ja 0.999.0, näkisit seuraavan, jos
ajaisit cargo update-komennon:
$ cargo update
Updating crates.io index
Locking 1 package to latest Rust 1.96.0 compatible version
Updating rand v0.10.1 -> v0.10.2 (available: v0.999.0)
Cargo jättää huomiotta version 0.999.0 julkaisun. Tässä vaiheessa huomaisit myös muutoksen
Cargo.lock-tiedostossasi, joka osoittaa, että käyttämäsi rand-craten versio on nyt 0.10.2. Käyttääksesi rand-version 0.999.0 tai mitä tahansa versiota 0.999.x-
sarjassa, sinun täytyisi päivittää Cargo.toml-tiedosto näyttämään tältä
sen sijaan (älä tee tätä muutosta, koska seuraavat esimerkit olettavat
käyttäväsi rand-versiota 0.10):
[dependencies]
rand = "0.999.0"
Seuraavalla cargo build -ajolla Cargo päivittää saatavilla olevien cratejen rekisterin
ja arvioi rand-vaatimuksesi uudelleen määrittämäsi uuden version
mukaan.
Cargosta ja sen ekosysteemistä on paljon enemmän sanottavaa, joita käsittelemme luvussa 14, mutta toistaiseksi se on kaikki mitä sinun täytyy tietää. Cargo tekee kirjastojen uudelleenkäytöstä hyvin helppoa, joten rustilaiset voivat kirjoittaa pienempiä projekteja, jotka koostuvat useista paketeista.
Satunnaisluvun generointi
Aloitetaan rand-craten käyttö luvun generoimiseksi arvattavaksi. Seuraava vaihe on
päivittää src/main.rs, kuten listauksessa 2-3.
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-03/src/main.rs:all}}
Ensin lisäämme rivin use rand::prelude::*;. prelude-moduuli sisältää
rand-craten yleisimmin käytetyt osat, ja use tuo nämä kohteet
ohjelmamme näkyvyysalueelle.
Seuraavaksi lisäämme kaksi riviä keskelle. Ensimmäisellä rivillä kutsumme
rand::rng-funktiota, joka antaa meille tietyn satunnaislukugeneraattorin,
jota käytämme: sellaisen, joka on paikallinen nykyiselle suoritussäikeelle
ja jonka käyttöjärjestelmä siemenöi. Sitten kutsumme random_range-
metodia satunnaislukugeneraattorilla. Tämän metodin määrittelee RngExt-trait, joka on
osa rand::prelude-moduulia, jonka toimme näkyviin use rand::prelude::*; -lauseella. random_range-metodi ottaa
alueen lausekkeena argumenttina ja generoi satunnaisluvun alueella. Käyttämämme
alueen lauseke on muodossa start..=end ja on sisältävä molemmissa rajoissa, joten meidän
täytyy määritellä 1..=100 pyytääksemme lukua väliltä 1–100.
Huom: Et vain tiedä, mitä traitteja käyttää ja mitä metodeja ja funktioita kutsua cratesta, joten jokaisella cratella on dokumentaatio ohjeineen käyttöön. Cargon toinen hieno ominaisuus on, että
cargo doc --open-komennon ajaminen kääntää kaikkien riippuvuuksiesi tarjoaman dokumentaation paikallisesti ja avaa sen selaimessasi. Jos olet kiinnostunut muusta toiminnallisuudestarand-cratessa, aja esimerkiksicargo doc --openja napsautarandvasemman sivupalkin kohdasta.
Toinen uusi rivi tulostaa salaisen luvun. Tämä on hyödyllistä ohjelmaa kehittäessä testataksemme sitä, mutta poistamme sen lopullisesta versiosta. Se ei ole kovin hauska peli, jos ohjelma tulostaa vastauksen heti käynnistyessään!
Kokeile ajaa ohjelma muutaman kerran:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
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` profile [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
Sinun pitäisi saada eri satunnaislukuja, ja niiden kaikkien pitäisi olla lukuja väliltä
1–100. Jos saat varoituksia, ne ovat turvallisia ohittaa. Jos saat virheitä,
tarkista, että sinulla on rand = "0.10.1" Cargo.toml-tiedostossasi, koska tulevat
rand-versiot voivat sisältää erilaisen API:n, mutta mikä tahansa versio 0.10-sarjassa
pitäisi toimia tämän luvun koodin kanssa.
Arvauksen vertaaminen salaiseen lukuun
Nyt kun meillä on käyttäjän syöte ja satunnainen luku, voimme vertailla niitä. Tämä vaihe näytetään listauksessa 2-4. Huomaa, että tämä koodi ei vielä käänny, kuten selitämme.
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-04/src/main.rs:here}}
Ensin lisäämme toisen use-lauseen tuoden tyypin nimeltä
std::cmp::Ordering näkyviin standardikirjastosta. Ordering-tyyppi
on toinen enum ja sillä on variantit Less, Greater ja Equal. Nämä ovat
kolme mahdollista tulosta, kun vertaat kahta arvoa.
Sitten lisäämme viisi uutta riviä pohjaan, jotka käyttävät Ordering-tyyppiä. cmp-metodi vertaa kahta arvoa ja sitä voi
kutsua millä tahansa, jota voi verrata. Se ottaa viittauksen siihen, mihin haluat verrata: tässä se
vertaa guess:ia secret_number:iin. Sitten se palauttaa Ordering-enumin variantin, jonka toimme näkyviin use-lauseella. Käytämme
match -lauseketta päättääksemme, mitä tehdä seuraavaksi sen perusteella,
mikä Ordering-variantti palautettiin cmp-kutsusta arvoilla
guess ja secret_number.
match-lauseke koostuu haaroista. Haara koostuu kuviosta,
jota vasten matchataan, ja koodista, joka pitäisi suorittaa, jos match-lausekkeelle annettu arvo
sopii haaran kuvioon. Rust ottaa match-lausekkeelle annetun arvon ja katsoo
jokaisen haaran kuviota vuorollaan. Kuviot ja match-rakenne ovat
voimakkaita Rust-ominaisuuksia: ne antavat sinun ilmaista monenlaisia tilanteita, joita koodisi
saattaa kohdata, ja varmistavat, että käsittelet ne kaikki. Nämä ominaisuudet käsitellään
tarkemmin luvuissa 6 ja 19.
Käydään läpi esimerkki käyttämällä tässä käyttämäämme match-lauseketta. Sanotaan, että
käyttäjä on arvannut 50 ja tällä kertaa generoitu salainen luku on
38.
Kun koodi vertaa 50:tä 38:een, cmp-metodi palauttaa
Ordering::Greater, koska 50 on suurempi kuin 38. match-lauseke saa
Ordering::Greater-arvon ja alkaa tarkistaa kunkin haaran kuviota. Se katsoo
ensimmäisen haaran kuviota Ordering::Less ja näkee, ettei arvo
Ordering::Greater matchaa Ordering::Less:ia, joten se jättää huomiotta koodin
kyseisessä haarassa ja siirtyy seuraavaan haaraan. Seuraavan haaran kuvio on
Ordering::Greater, joka matchaa Ordering::Greater:in! Kyseiseen haaraan
liittyvä koodi suoritetaan ja tulostaa Too big! näytölle. match-
lauseke päättyy ensimmäisen onnistuneen matchin jälkeen, joten se ei katso viimeistä
haaraa tässä skenaariossa.
Listauksen 2-4 koodi ei kuitenkaan vielä käänny. Kokeillaan sitä:
{{#include ../listings/ch02-guessing-game-tutorial/listing-02-04/output.txt}}
Virheen ydin sanoo, että tyypit eivät täsmää. Rustilla on
vahva, staattinen tyyppijärjestelmä. Sillä on kuitenkin myös tyyppipäättely. Kun kirjoitimme
let mut guess = String::new(), Rust pystyi päättelemään, että guess:n pitäisi olla
String eikä pakottanut meitä kirjoittamaan tyyppiä. secret_number sen sijaan
on lukutyyppi. Muutama Rustin lukutyyppi voi olla arvolla välillä 1
ja 100: i32, 32-bittinen luku; u32, etumerkittömä 32-bittinen luku; i64, 64-
bittinen luku; ja muita. Ellei toisin määritellä, Rust oletuksena käyttää
i32:ta, joka on secret_number:n tyyppi, ellei lisää tyyppitietoa muualla,
mikä saisi Rustin päättelemään eri numeerisen tyypin. Virheen syy
on, että Rust ei voi verrata merkkijonoa ja lukutyyppiä.
Lopulta haluamme muuntaa String-merkkijonon, jonka ohjelma lukee syötteenä, lukutyyppiin,
jotta voimme verrata sitä numeerisesti salaiseen lukuun. Teemme sen
lisäämällä tämän rivin main-funktion runkoon:
Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-03-convert-string-to-number/src/main.rs:here}}
Rivi on:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
Luomme muuttujan nimeltä guess. Mutta odota, eikö ohjelmalla jo ole
muuttujaa nimeltä guess? On, mutta Rust sallii meille hyödyllisesti varjostaa
guess:n aiemman arvon uudella. Varjostus antaa meidän käyttää uudelleen guess-
muuttujan nimeä sen sijaan, että pakottaisimme meidät luomaan kaksi erillistä muuttujaa, kuten
guess_str ja guess. Käsittelemme tätä tarkemmin
luvussa 3, mutta toistaiseksi tiedä, että tätä ominaisuutta
käytetään usein, kun haluat muuntaa arvon tyypistä toiseen.
Sidoimme tämän uuden muuttujan lausekkeeseen guess.trim().parse(). guess
lausekkeessa viittaa alkuperäiseen guess-muuttujaan, joka sisälsi
syötteen merkkijonona. trim-metodi String-instanssilla poistaa kaiken
väliylityksen alusta ja lopusta, mikä meidän täytyy tehdä ennen kuin voimme muuntaa
merkkijonon u32:ksi, joka voi sisältää vain numeerista dataa. Käyttäjän täytyy painaa
enter tyydyttääkseen read_line-metodin ja syöttääkseen arvauksensa, mikä lisää
rivinvaihtomerkin merkkijonoon. Jos käyttäjä kirjoittaa 5 ja
painaa enter, guess näyttää tältä: 5\n. \n edustaa
”rivinvaihtoa.” (Windowsissa enter-näppäimen painaminen tuottaa rivinvaihdon
ja rivinvaihdon, \r\n.) trim-metodi poistaa \n:n tai \r\n:n, jolloin
jäljelle jää vain 5.
Merkkijonojen parse-metodi muuntaa merkkijonon
toiseen tyyppiin. Tässä käytämme sitä muuntamaan merkkijonosta luvuksi. Meidän täytyy
kertoa Rustille tarkka lukutyyppi, jota haluamme käyttämällä let guess: u32. Kaksoispiste
(:) guess:n jälkeen kertoo Rustille, että annotoimme muuttujan tyypin. Rustilla on
muutama sisäänrakennettu lukutyyppi; tässä nähty u32 on etumerkittömä 32-bittinen kokonaisluku.
Se on hyvä oletusvalinta pienelle positiiviselle luvulle. Opit muista
lukutyypeistä luvussa 3.
Lisäksi tämän esimerkkiohjelman u32-annotaatio ja vertailu
secret_number:iin tarkoittavat, että Rust päättelee secret_number:n olevan myös
u32. Joten nyt vertailu on kahden saman
tyypin arvon välillä!
parse-metodi toimii vain merkeillä, jotka voidaan loogisesti muuntaa
luvuiksi, ja voi siksi helposti aiheuttaa virheitä. Jos esimerkiksi merkkijono
sisältäisi A👍%, sitä ei voisi muuntaa luvuksi. Koska se
saattaa epäonnistua, parse-metodi palauttaa Result-tyypin, aivan kuten read_line-
metodi (käsiteltiin aiemmin ”Mahdollisen epäonnistumisen käsittely
Result-tyypillä” -osiossa). Käsittelemme
tämän Result-arvon samalla tavalla käyttämällä jälleen expect-metodia. Jos parse
palauttaa Err-Result-variantin, koska se ei voinut luoda lukua
merkkijonosta, expect-kutsu kaataa pelin ja tulostaa antamamme viestin.
Jos parse voi onnistuneesti muuntaa merkkijonon luvuksi, se palauttaa
Ok-variantin Result-tyypistä, ja expect palauttaa haluamamme luvun
Ok-arvosta.
Ajetaan ohjelma nyt:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
Hienoa! Vaikka arvauksen eteen lisättiin välilyöntejä, ohjelma silti selvitti, että käyttäjä arvasi 76. Aja ohjelma muutaman kerran varmistaaksesi erilaisen käyttäytymisen eri syötteillä: arvaa luku oikein, arvaa liian suuri luku ja arvaa liian pieni luku.
Meillä on nyt suurin osa pelistä toiminnassa, mutta käyttäjä voi tehdä vain yhden arvauksen. Muutetaan sitä lisäämällä silmukka!
Useiden arvausten salliminen silmukalla
loop-avainsana luo äärettömän silmukan. Lisäämme silmukan antaaksemme käyttäjille
enemmän mahdollisuuksia arvata luku:
Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-04-looping/src/main.rs:here}}
Kuten näet, olemme siirtäneet kaiken arvauskehotteesta eteenpäin silmukkaan. Muista sisentää silmukan sisällä olevat rivit neljä välilyöntiä lisää ja aja ohjelma uudelleen. Ohjelma pyytää nyt uutta arvausta ikuisesti, mikä itse asiassa tuo uuden ongelman. Käyttäjä ei näytä pystyvän lopettamaan!
Käyttäjä voisi aina keskeyttää ohjelman näppäinyhdistelmällä
ctrl-c. Mutta on toinen tapa paeta tämä tyydyttämätön
hirviö, kuten mainittiin parse-keskustelussa ”Arvauksen vertaaminen salaiseen lukuun” -osiossa: jos
käyttäjä syöttää ei-numeerisen vastauksen, ohjelma kaatuu. Voimme hyödyntää
sitä salliaksemme käyttäjän lopettaa, kuten tässä:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
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 src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
quit-kirjoittaminen lopettaa pelin, mutta kuten huomaat, myös mikä tahansa
muu ei-numeerinen syöte lopettaa sen. Tämä on lievästi sanottuna epäoptimaalista; haluamme pelin
pysähtyvän myös, kun oikea luku arvataan.
Lopettaminen oikean arvauksen jälkeen
Ohjelmoidaan peli lopettamaan, kun käyttäjä voittaa, lisäämällä break-lause:
Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-05-quitting/src/main.rs:here}}
break-rivin lisääminen You win!-tekstin jälkeen saa ohjelman poistumaan silmukasta, kun
käyttäjä arvaa salaisen luvun oikein. Silmukasta poistuminen tarkoittaa myös
ohjelman päättymistä, koska silmukka on main-funktion viimeinen osa.
Virheellisen syötteen käsittely
Hienosäätääksemme pelin käyttäytymistä sen sijaan, että kaataisimme ohjelman, kun
käyttäjä syöttää ei-numeerisen arvon, tehdään pelistä sellainen, että se jättää huomiotta ei-numeerisen syötteen, jotta
käyttäjä voi jatkaa arvaamista. Voimme tehdä sen muuttamalla riviä, jossa guess
muunnetaan String:stä u32:ksi, kuten listauksessa 2-5.
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:here}}
Vaihdamme expect-kutsusta match-lausekkeeseen siirtyäksemme kaatumisesta
virheenkäsittelyyn. Muista, että parse palauttaa Result-tyypin
ja Result on enum, jolla on variantit Ok ja Err. Käytämme
tässä match-lauseketta, kuten teimme cmp-metodin Ordering-tuloksen kanssa.
Jos parse pystyy onnistuneesti muuttamaan merkkijonon luvuksi, se
palauttaa Ok-arvon, joka sisältää tuloksena olevan luvun. Tuo Ok-arvo
matchaa ensimmäisen haaran kuvion, ja match-lauseke palauttaa vain
num-arvon, jonka parse tuotti ja laittoi Ok-arvon sisään. Tuo luku
päätyy juuri sinne, minne haluamme uudessa guess-muuttujassa, jonka luomme.
Jos parse ei pysty muuttamaan merkkijonoa luvuksi, se palauttaa
Err-arvon, joka sisältää lisätietoa virheestä. Err-arvo
ei matchaa ensimmäisen match-haaran kuviota Ok(num), mutta se
matchaa toisen haaran kuvion Err(_). Alaviiva _ on
catch-all-arvo; tässä esimerkissä sanomme haluavamme matchata kaikki Err-
arvot riippumatta siitä, mitä tietoa niissä on. Joten ohjelma
suorittaa toisen haaran koodin continue, joka kertoo ohjelman siirtyä
seuraavaan silmukan iteratioon ja pyytää uutta arvausta. Eli käytännössä
ohjelma jättää huomiotta kaikki virheet, joita parse saattaa kohdata!
Nyt kaiken pitäisi toimia odotetusti. Kokeillaan:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
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!
Mahtavaa! Pienellä viimeisellä hienosäädöllä viimeistelemme arvauspelin. Muista,
että ohjelma tulostaa edelleen salaisen luvun. Se toimi hyvin
testauksessa, mutta se pilaa pelin. Poistetaan println!, joka tulostaa
salainen luvun. Listausta 2-6 näyttää lopullisen koodin.
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-06/src/main.rs}}
Tässä vaiheessa olet onnistuneesti rakentanut arvauspelin. Onnittelut!
Yhteenveto
Tämä projekti oli käytännönläheinen tapa esitellä sinulle monia uusia Rust-käsitteitä:
let, match, funktiot, ulkoisten cratejen käyttö ja paljon muuta. Seuraavissa
luvuissa opit näistä käsitteistä tarkemmin. Luku 3
käsittelee käsitteitä, joita useimmissa ohjelmointikielissä on, kuten muuttujat, data-
tyypit ja funktiot, ja näyttää, miten niitä käytetään Rustissa. Luku 4 tutkii
omistajuutta, ominaisuutta, joka tekee Rustista erilaisen kuin muut kielet. Luku 5
käsittelee rakenteita ja metodisyntaksia, ja luku 6 selittää, miten enumit toimivat.