Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:all}}
Listing 2-1: Koodi, joka hakee arvauksen käyttäjältä ja tulostaa sen

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
Listing 2-2: Tuloste cargo build -komennon ajosta rand-craten lisäämisen jälkeen riippuvuudeksi

Saatat 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.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-03/src/main.rs:all}}
Listing 2-3: Koodin lisääminen satunnaisluvun generoimiseksi

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 toiminnallisuudesta rand-cratessa, aja esimerkiksi cargo doc --open ja napsauta rand vasemman 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.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-04/src/main.rs:here}}
Listing 2-4: Kahden luvun vertailun mahdollisten palautusarvojen käsittely

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.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:here}}
Listing 2-5: Ei-numeerisen arvauksen huomiotta jättäminen ja uuden arvauksen pyytäminen ohjelman kaatamisen sijaan

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.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-06/src/main.rs}}
Listing 2-6: Valmis arvauspeli-koodi

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.