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

Rust-ohjelmointikieli

kirjoittanut Steve Klabnik, Carol Nichols ja Chris Krycho, Rust-yhteisön kontribuutioilla

Tämä tekstin versio olettaa, että käytät Rustia 1.96.0 (julkaistu 28.5.2026) tai uudempaa versiota ja että kaikissa projekteissa on Cargo.toml-tiedostossa edition = "2024", jotta ne käyttävät Rust 2024 -editionin idiomeja. Katso luvun 1 ”Asennus”-osio ohjeista Rustin asentamiseen tai päivittämiseen ja liite E tietoa editioneista.

HTML-muoto on saatavilla verkossa osoitteessa https://doc.rust-lang.org/stable/book/ ja offline-tilassa rustup-asennuksen mukana; avaa kirja komennolla rustup doc --book.

Useita yhteisön [käännöksiä] on myös saatavilla.

Teksti on saatavilla No Starch Pressin painettuna ja e-kirjana.

🚨 Haluatko interaktiivisemman oppimiskokemuksen? Kokeile toista versiota Rust-kirjasta, jossa on: kyselyitä, korostuksia, visualisointeja ja muuta: https://rust-book.cs.brown.edu

Esipuhe

Rust-ohjelmointikieli on edennyt pitkälle muutamassa vuodessa pienestä ja vasta syntyneestä harrastajayhteisöstä yhdeksi maailman rakastetuimmista ja kysytyimmistä ohjelmointikielistä. Taaksepäin katsottuna oli väistämätöntä, että Rustin voima ja lupaus herättäisivät huomiota ja saisivat jalansijaa järjestelmäohjelmoinnissa. Se, mikä ei ollut väistämätöntä, oli globaali kiinnostuksen ja innovaation kasvu, joka levisi avoimen lähdekoodin yhteisöihin ja nopeutti laajaa käyttöönottoa eri toimialoilla.

Tässä vaiheessa on helppo viitata Rustin upeisiin ominaisuuksiin selittääkseen tämän kiinnostuksen ja käyttöönoton räjähdysmäisen kasvun. Kuka ei haluaisi muistiturvallisuutta, ja nopeaa suorituskykyä, ja ystävällistä kääntäjää, ja loistavia työkaluja — muiden upeiden ominaisuuksien ohella? Rust-kieli, jota näet tänään, yhdistää vuosien tutkimuksen järjestelmäohjelmoinnissa elävän ja intohimoisen yhteisön käytännön viisauden kanssa. Kieli on suunniteltu tarkoituksella ja hiottu huolella, tarjoten kehittäjille työkalun, joka helpottaa turvallisen, nopean ja luotettavan koodin kirjoittamista.

Mutta mikä tekee Rustista todella erityisen, ovat sen juuret voimaannuttaa sinut saavuttamaan tavoitteesi. Tämä on kieli, joka haluaa sinun onnistuvan, ja voimaannuttamisen periaate kulkee läpi yhteisön, joka rakentaa, ylläpitää ja edistää tätä kieltä. Edellisen painoksen jälkeen Rust on kehittynyt entistä enemmän aidosti globaaliksi ja luotettavaksi kieleksi. Rust-projektia tukee nyt vankasti Rust Foundation, joka investoi myös keskeisiin aloitteisiin varmistaakseen, että Rust on turvallinen, vakaa ja kestävä.

Tämä painos Rust-ohjelmointikielestä on kattava päivitys, joka heijastaa kielen kehitystä vuosien varrella ja tarjoaa arvokasta uutta tietoa. Se ei kuitenkaan ole vain opas syntaksiin ja kirjastoihin — se on kutsu liittyä yhteisöön, joka arvostaa laatua, suorituskykyä ja harkittua suunnittelua. Olitpa kokenut kehittäjä, joka tutustuu Rustiin ensimmäistä kertaa, tai kokenut rustilainen, joka haluaa hioa taitojaan, tästä painoksesta on hyötyä kaikille.

Rust-matka on ollut yhteistyötä, oppimista ja iterointia. Kielen ja sen ekosysteemin kasvu heijastaa suoraan sen elävää ja monimuotoista yhteisöä. Tuhansien kehittäjien panos — ydinkielen suunnittelijoista satunnaisiin kontribuutioihin — tekee Rustista ainutlaatuisen ja tehokkaan työkalun. Ottamalla tämän kirjan käteesi et vain opi uutta ohjelmointikieltä — liityt liikkeeseen, joka pyrkii tekemään ohjelmistoista parempia, turvallisempia ja miellyttävämpiä työstää.

Tervetuloa Rust-yhteisöön!

  • Bec Rumbul, Rust Foundationin toimitusjohtaja

Johdanto

Huom: Tämä kirjan painos on sama kuin The Rust Programming Language, joka on saatavilla painettuna ja e-kirjana No Starch Press -kustantajalta.

Tervetuloa lukemaan The Rust Programming Language -kirjaa, joka on johdatus Rust-ohjelmointikieleen. Rust-ohjelmointikieli auttaa sinua kirjoittamaan nopeampaa ja luotettavampaa ohjelmistoa. Korkean tason ergonomia ja matalan tason hallinta ovat usein ristiriidassa ohjelmointikielten suunnittelussa; Rust haastaa tämän ristiriidan. Tasapainottamalla tehokasta teknistä kapasiteettia ja erinomaista kehittäjäkokemusta Rust antaa sinulle mahdollisuuden hallita matalan tason yksityiskohtia (kuten muistin käyttöä) ilman perinteisesti näihin liittyvää vaivannäköä.

Kenelle Rust sopii

Rust sopii monille ihmisille monista syistä. Katsotaan muutamia tärkeimpiä ryhmiä.

Kehittäjätiimit

Rust on osoittautunut tuottavaksi työkaluksi suurten kehittäjätiimien yhteistyöhön, joissa on eri tasoisia järjestelmäohjelmoinnin osaamista. Matalan tason koodi on altis monille hienovaraisille bugeille, jotka useimmissa muissa kielissä havaitaan vasta laajan testauksen ja kokeneiden kehittäjien huolellisen koodikatselmuksen kautta. Rustissa kääntäjä toimii portinvartijana kieltäytymällä kääntämästä koodia, jossa on näitä vaikeasti havaittavia bugeja, mukaan lukien rinnakkaisuusbugeja. Työskennellessään kääntäjän rinnalla tiimi voi keskittyä ohjelman logiikkaan sen sijaan, että jahtaisi bugeja.

Rust tuo myös nykyaikaiset kehittäjätyökalut järjestelmäohjelmoinnin maailmaan:

  • Cargo, mukana tuleva riippuvuuksien hallintatyökalu ja build-työkalu, tekee riippuvuuksien lisäämisestä, kääntämisestä ja hallinnasta vaivatonta ja yhtenäistä koko Rust-ekosysteemissä.
  • rustfmt-muotoilutyökalu varmistaa yhtenäisen koodaustyylin eri kehittäjien välillä.
  • Rust Language Server tarjoaa integroidun kehitysympäristön (IDE) tuen koodin täydennykselle ja rivikohtaisille virheilmoituksille.

Käyttämällä näitä ja muita Rust-ekosysteemin työkaluja kehittäjät voivat olla tuottavia kirjoittaessaan järjestelmätason koodia.

Opiskelijat

Rust sopii opiskelijoille ja niille, jotka ovat kiinnostuneita oppimaan järjestelmäkäsitteistä. Rustin avulla monet ovat oppineet aiheita, kuten käyttöjärjestelmien kehitystä. Yhteisö on erittäin vieraanvarainen ja vastaa mielellään opiskelijoiden kysymyksiin. Tämän kirjan kaltaisten ponnistusten kautta Rust-tiimit haluavat tehdä järjestelmäkäsitteistä helpommin saavutettavia useammille ihmisille, erityisesti ohjelmointiin vasta tutustuville.

Yritykset

Satoja yrityksiä, suuria ja pieniä, käyttää Rustia tuotannossa monenlaisiin tehtäviin, mukaan lukien komentorivityökalut, web-palvelut, DevOps-työkalut, sulautetut laitteet, äänen ja videon analysointi ja transkoodaus, kryptovaluutat, bioinformatiikka, hakukoneet, esineiden internet -sovellukset, koneoppiminen ja jopa merkittäviä osia Firefox-selaimesta.

Avoimen lähdekoodin kehittäjät

Rust on niille, jotka haluavat rakentaa Rust-ohjelmointikieltä, yhteisöä, kehittäjätyökaluja ja kirjastoja. Haluaisimme mielellämme sinun osallistuvan Rust-kielen kehitykseen.

Nopeutta ja vakautta arvostavat

Rust on niille, jotka kaipaavat nopeutta ja vakautta kielessä. Nopeudella tarkoitamme sekä sitä, kuinka nopeasti Rust-koodi voi suorittua, että sitä, kuinka nopeasti Rustin avulla voi kirjoittaa ohjelmia. Rust-kääntäjän tarkistukset varmistavat vakauden ominaisuuksien lisäysten ja refaktoroinnin aikana. Tämä eroaa hauraasta legacy-koodista kielissä, joissa näitä tarkistuksia ei ole ja jota kehittäjät usein pelkäävät muokata. Pyrkimällä nollakustannuksisiin abstraktioihin— korkean tason ominaisuuksiin, jotka kääntyvät matalan tason koodiksi yhtä nopeasti kuin käsin kirjoitettu koodi—Rust pyrkii tekemään turvallisesta koodista myös nopeaa koodia.

Rust-kieli toivoo tukevansa monia muitakin käyttäjiä; tässä mainitut ovat vain joitakin suurimpia sidosryhmiä. Kaiken kaikkiaan Rustin suurin tavoite on poistaa kompromissit, jotka ohjelmoijat ovat hyväksyneet vuosikymmeniä, tarjoamalla turvallisuuden ja tuottavuuden, nopeuden ja ergonomian. Kokeile Rustia ja katso, toimivatko sen valinnat sinulle.

Kenelle tämä kirja on tarkoitettu

Tämä kirja olettaa, että olet kirjoittanut koodia toisella ohjelmointikielellä, mutta se ei tee oletuksia siitä, millä kielellä. Olemme pyrkineet tekemään materiaalista laajasti saavutettavaa erilaisista ohjelmointitaustoista tuleville. Emme käytä paljon aikaa siihen, mitä ohjelmointi on tai miten siitä ajatellaan. Jos olet täysin uusi ohjelmoinnissa, saat paremman hyödyn kirjasta, joka on erityisesti suunnattu ohjelmoinnin johdatukseksi.

Kuinka käyttää tätä kirjaa

Yleensä tämä kirja olettaa, että luet sen järjestyksessä alusta loppuun. Myöhemmät luvut rakentuvat aiempien lukujen käsitteiden varaan, ja aiemmat luvut eivät välttämättä syvenny tiettyyn aiheeseen, vaan palaavat siihen myöhemmässä luvussa.

Löydät tästä kirjasta kahta lajia lukuja: käsite- ja projektilukuja. Käsite-luvuissa opit Rustin jonkin puolen. Projektiluvuissa rakennamme yhdessä pieniä ohjelmia soveltaen tähän mennessä oppimaasi. Luvut 2, 12 ja 21 ovat projektilukuja; muut ovat käsite-lukuja.

Luku 1 selittää, miten Rust asennetaan, miten kirjoitetaan “Hello, world!” -ohjelma ja miten käytetään Cargo-työkalua, Rustin paketinhallintaa ja build-työkalua. Luku 2 on käytännönläheinen johdatus Rust-ohjelman kirjoittamiseen, jossa rakennat arvauspeliä. Tässä käsittelemme asioita korkealla tasolla, ja myöhemmät luvut tarjoavat lisätietoa. Jos haluat päästä heti käsiksi koodiin, Luku 2 on oikea paikka. Jos olet erityisen huolellinen oppija, joka haluaa oppia jokaisen yksityiskohdan ennen seuraavaan siirtymistä, voit ohittaa Luvun 2 ja siirtyä suoraan Lukuun 3, joka käsittelee Rustin ominaisuuksia, jotka ovat samankaltaisia kuin muissa ohjelmointikielissä; voit sitten palata Lukuun 2, kun haluat työskennellä projektin parissa soveltaen oppimaasi.

Luvussa 4 opit Rustin omistajuusjärjestelmästä. Luku 5 käsittelee structeja ja metodeja. Luku 6 käsittelee enumeja, match-lausekkeita sekä if let- ja let...else-ohjausrakenteita. Käytät structeja ja enumeja mukautettujen tyyppien luomiseen.

Luvussa 7 opit Rustin moduulijärjestelmästä ja yksityisyysäännöistä koodin ja sen julkisen sovellusohjelmointirajapinnan (API) järjestämiseen. Luku 8 käsittelee joitakin yleisiä kokoelmien tietorakenteita, joita standardikirjasto tarjoaa: vektoreita, merkkijonoja ja hash-taulukoita. Luku 9 tutkii Rustin virheenkäsittelyfilosofiaa ja -tekniikoita.

Luku 10 syventyy geneerisyyteen, traitteihin ja elinikäihin, jotka antavat sinulle mahdollisuuden määritellä koodia, joka soveltuu useille tyypeille. Luku 11 käsittelee testausta, joka on tarpeen Rustin turvallisuustakuista huolimatta varmistamaan ohjelmasi logiikan oikeellisuus. Luvussa 12 rakennamme oman toteutuksemme osasta grep-komentorivityökalun toiminnallisuudesta, joka etsii tekstiä tiedostoista. Tähän käytämme monia aiemmissa luvuissa käsittelemiämme käsitteitä.

Luku 13 tutkii sulkeumia ja iteraattoreita: Rustin ominaisuuksia, jotka tulevat funktionaalisista ohjelmointikielistä. Luvussa 14 tarkastelemme Cargo-työkalua tarkemmin ja puhumme parhaista käytännöistä kirjastojesi jakamiseen muiden kanssa. Luku 15 käsittelee älykkäitä osoittimia, joita standardikirjasto tarjoaa, ja traitteja, jotka mahdollistavat niiden toiminnallisuuden.

Luvussa 16 käymme läpi erilaisia rinnakkaisohjelmoinnin malleja ja puhumme siitä, miten Rust auttaa sinua ohjelmoimaan useissa säikeissä pelottomasti. Luvussa 17 rakennamme tämän päälle tutkien Rustin async- ja await-syntaksia sekä tehtäviä, futureja ja streameja ja niiden mahdollistamaa kevyttä rinnakkaisuusmallia.

Luku 18 tarkastelee, miten Rustin idiomit vertautuvat tuttuihin olio-ohjelmoinnin periaatteisiin. Luku 19 on viite kuvioihin ja kuvioiden täsmäyttämiseen, jotka ovat tehokkaita tapoja ilmaista ideoita Rust-ohjelmissa. Luku 20 sisältää valikoiman edistyneitä aiheita, mukaan lukien unsafe Rust, makrot ja lisätietoa elinikäistä, traitteista, tyypeistä, funktioista ja sulkeumista.

Luvussa 21 viimeistelemme projektin, jossa toteutamme matalan tason monisäikeisen web-palvelimen!

Lopuksi joissakin liitteissä on hyödyllistä tietoa kielestä viitemuotoisemmassa muodossa. Liite A käsittelee Rustin avainsanoja, Liite B Rustin operaattoreita ja symboleja, Liite C standardikirjaston tarjoamia johdettavia traitteja, Liite D hyödyllisiä kehitystyökaluja ja Liite E selittää Rustin editioneja. Liitteestä F löydät kirjan käännökset, ja Liitteessä G käsittelemme, miten Rustia tehdään ja mitä nightly Rust on.

Tätä kirjaa voi lukea monella tavalla: jos haluat hypätä eteenpäin, tee se! Saatat joutua palaamaan aiempiin lukuihin, jos kohtaat hämmennystä. Tee kuitenkin miten parhaalta tuntuu.

Tärkeä osa Rustin oppimista on oppia lukemaan kääntäjän näyttämiä virheilmoituksia: ne ohjaavat sinua kohti toimivaa koodia. Siksi tarjoamme monia esimerkkejä, jotka eivät käänny, sekä kääntäjän näyttämän virheilmoituksen kussakin tilanteessa. Huomaa, että jos kirjoitat ja suoritat satunnaisen esimerkin, se ei välttämättä käänny! Varmista, että luet ympäröivän tekstin nähdäksesi, onko esimerkki, jota yrität suorittaa, tarkoitettu virheeseen. Useimmissa tilanteissa ohjaamme sinut oikeaan versioon koodista, joka ei käänny. Ferris auttaa myös erottamaan koodin, joka ei ole tarkoitettu toimimaan:

FerrisMerkitys
Ferris with a question markTämä koodi ei käänny!
Ferris throwing up their handsTämä koodi panikoi!
Ferris with one claw up, shruggingTämä koodi ei tuota haluttua käyttäytymistä.

Useimmissa tilanteissa ohjaamme sinut oikeaan versioon koodista, joka ei käänny.

Lähdekoodi

Tämän kirjan lähdekooditiedostot löytyvät GitHubista.

Aloittaminen

Aloitetaan Rustin matka! On paljon opittavaa, mutta jokainen matka alkaa jostain. Tässä luvussa käsittelemme:

  • Rustin asentaminen Linuxille, macOS:lle ja Windowsille
  • Ohjelman kirjoittaminen, joka tulostaa Hello, world!
  • cargo-työkalun käyttö, joka on Rustin paketinhallinta- ja rakennusjärjestelmä

Asennus

Asennus

Ensimmäinen askel on Rustin asentaminen. Lataamme Rustin rustup-työkalun kautta, joka on komentorivityökalu Rust-versioiden ja niihin liittyvien työkalujen hallintaan. Tarvitset internet-yhteyden latausta varten.

Huom: Jos et halua käyttää rustup-työkalua jostain syystä, katso muut Rustin asennustavat -sivulta lisää vaihtoehtoja.

Seuraavat vaiheet asentavat uusimman vakaan version Rust-kääntäjästä. Rustin vakaustakuut varmistavat, että kaikki tämän kirjan esimerkit, jotka kääntyvät, kääntyvät edelleen myös uudemmissa Rust-versioissa. Tuloste voi hieman erota versioiden välillä, koska Rust parantaa virheilmoituksia ja varoituksia säännöllisesti. Toisin sanoen, mikä tahansa näillä ohjeilla asennettu uudempi vakaa Rust-versio toimii odotetusti tämän kirjan sisällön kanssa.

Komentorivimerkinnät

Tässä luvussa ja koko kirjassa näytämme terminaalissa käytettäviä komentoja. Rivit, jotka sinun pitäisi kirjoittaa terminaaliin, alkavat merkillä $. Sinun ei tarvitse kirjoittaa $-merkkiä; se on komentorivin kehote, joka osoittaa kunkin komennon alun. Rivit, jotka eivät ala $-merkillä, näyttävät yleensä edellisen komennon tulosteen. Lisäksi PowerShell-esimerkeissä käytetään >-merkkiä $-merkin sijasta.

rustup-asennus Linuxilla tai macOS:llä

Jos käytät Linuxia tai macOS:ää, avaa terminaali ja kirjoita seuraava komento:

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Komento lataa skriptin ja käynnistää rustup-työkalun asennuksen, joka asentaa uusimman vakaan Rust-version. Sinua saatetaan pyytää salasanaasi. Jos asennus onnistuu, näet seuraavan rivin:

Rust is installed now. Great!

Tarvitset myös linkkerin, joka on ohjelma, jota Rust käyttää yhdistääkseen kääntämänsä tulosteet yhdeksi tiedostoksi. Sinulla on todennäköisesti jo sellainen. Jos saat linkkerivirheitä, sinun pitäisi asentaa C-kääntäjä, joka yleensä sisältää linkkerin. C-kääntäjä on hyödyllinen myös siksi, että jotkin yleiset Rust-paketit riippuvat C-koodista ja tarvitsevat C-kääntäjän.

macOS:llä voit hankkia C-kääntäjän suorittamalla:

$ xcode-select --install

Linux-käyttäjien tulisi yleensä asentaa GCC tai Clang jakelunsa dokumentaation mukaisesti. Esimerkiksi Ubuntussa voit asentaa build-essential-paketin.

rustup-asennus Windowsilla

Windowsissa siirry osoitteeseen https://www.rust-lang.org/tools/install ja seuraa Rustin asennusohjeita. Asennuksen aikana sinua pyydetään asentamaan Visual Studio. Se tarjoaa linkkerin ja natiivit kirjastot, joita ohjelmien kääntäminen tarvitsee. Jos tarvitset lisäapua tässä vaiheessa, katso https://rust-lang.github.io/rustup/installation/windows-msvc.html.

Tämän kirjan loppuosa käyttää komentoja, jotka toimivat sekä cmd.exe:ssä että PowerShellissä. Jos on erityisiä eroja, selitämme, kumpaa käyttää.

Vianmääritys

Voit tarkistaa, onko Rust asennettu oikein, avaamalla komentorivin ja kirjoittamalla:

$ rustc --version

Sinun pitäisi nähdä versionumero, commit-hash ja commit-päivämäärä uusimmalle julkaistulle vakaalle versiolle seuraavassa muodossa:

rustc x.y.z (abcabcabc yyyy-mm-dd)

Jos näet nämä tiedot, Rust on asennettu onnistuneesti! Jos et näe niitä, tarkista, että Rust on %PATH%-järjestelmämuuttujassasi seuraavasti.

Windows CMD:ssä käytä:

> echo %PATH%

PowerShellissä käytä:

> echo $env:Path

Linuxissa ja macOS:ssä käytä:

$ echo $PATH

Jos kaikki on oikein ja Rust ei silti toimi, voit saada apua monista paikoista. Ota selvää, miten voit olla yhteydessä muihin Rustaceaneihin (leikkisä lempinimi, jolla kutsumme itseämme) yhteisösivulta.

Päivittäminen ja poistaminen

Kun Rust on asennettu rustup-työkalun kautta, uuteen julkaistuun versioon päivittäminen on helppoa. Suorita komentorivillä seuraava päivitysskripti:

$ rustup update

Poistaaksesi Rustin ja rustup-työkalun, suorita seuraava poistoskripti komentorivillä:

$ rustup self uninstall

Paikallisen dokumentaation lukeminen

Rustin asennuksen mukana tulee paikallinen kopio dokumentaatiosta, joten voit lukea sitä offline-tilassa. Avaa paikallinen dokumentaatio selaimessasi komennolla rustup doc.

Aina kun standardikirjasto tarjoaa tyypin tai funktion etkä ole varma, mitä se tekee tai miten sitä käytetään, käytä sovellusohjelmointirajapinnan (API) dokumentaatiota selvittääksesi!

Tekstieditorien ja IDE:iden käyttö

Tämä kirja ei tee oletuksia siitä, mitä työkaluja käytät Rust-koodin kirjoittamiseen. Melkein mikä tahansa tekstieditori hoitaa homman! Monet tekstieditorit ja integroidut kehitysympäristöt (IDE) tarjoavat kuitenkin sisäänrakennettua Rust-tukea. Löydät melko ajantasaisen listan monista editoreista ja IDE:istä työkalusivulta Rustin verkkosivustolla.

Tämän kirjan käyttö offline-tilassa

Useissa esimerkeissä käytämme Rust-paketteja standardikirjaston ulkopuolelta. Työskennelläksesi näiden esimerkkien parissa tarvitset joko internet-yhteyden tai olet ladannut riippuvuudet etukäteen. Ladataksesi riippuvuudet etukäteen voit suorittaa seuraavat komennot. (Selitämme myöhemmin tarkemmin, mikä cargo on ja mitä kukin komento tekee.)

$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add rand@0.10.1 trpl@0.2.0

Tämä tallentaa näiden pakettien lataukset välimuistiin, joten sinun ei tarvitse ladata niitä myöhemmin. Kun olet suorittanut tämän komennon, sinun ei tarvitse säilyttää get-dependencies-kansiota. Jos olet suorittanut tämän komennon, voit käyttää --offline-lippua kaikissa cargo-komennoissa kirjan loppuosassa käyttääksesi näitä välimuistissa olevia versioita verkon käytön sijaan.

Hei, maailma!

Hello, World!

Nyt kun olet asentanut Rustin, on aika kirjoittaa ensimmäinen Rust-ohjelmasi. Uutta kieltä opiskellessa on perinteistä kirjoittaa pieni ohjelma, joka tulostaa tekstin Hello, world! näytölle, joten teemme saman tässä!

Huom: Tämä kirja olettaa perustuntemusta komentorivistä. Rust ei aseta erityisiä vaatimuksia editorillesi, työkaluillesi tai koodisi sijainnille, joten jos haluat käyttää IDE:tä komentorivin sijaan, käytä vapaasti suosikki-IDE:täsi. Monilla IDE:illä on nyt jonkin verran Rust-tukea; katso IDE:n dokumentaatiosta yksityiskohdat. Rust-tiimi on keskittynyt erinomaisen IDE-tuen mahdollistamiseen rust-analyzer-työkalun kautta. Katso Liite D lisätietoja varten.

Projektikansion luominen

Aloitat luomalla kansion Rust-koodillesi. Rustille ei ole väliä, missä koodisi sijaitsee, mutta tämän kirjan harjoituksia ja projekteja varten suosittelemme projects-kansion luomista kotihakemistoosi ja kaikkien projektiesi säilyttämistä siellä.

Avaa terminaali ja kirjoita seuraavat komennot luodaksesi projects-kansion ja “Hello, world!” -projektin kansion projects-kansion sisään.

Linuxissa, macOS:ssä ja PowerShellissä Windowsilla kirjoita tämä:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Windows CMD:ssä kirjoita tämä:

> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world

Rust-ohjelman perusteet

Seuraavaksi luo uusi lähdekooditiedosto ja kutsu sitä main.rs. Rust-tiedostot päättyvät aina .rs-päätteeseen. Jos käytät tiedostonimessä useampaa sanaa, käytä niiden erottamiseen alaviivaa. Käytä esimerkiksi hello_world.rs eikä helloworld.rs.

Avaa juuri luomasi main.rs-tiedosto ja kirjoita siihen Listauksen 1-1 koodi.

Filename: main.rs
fn main() {
    println!("Hello, world!");
}
Listing 1-1: Ohjelma, joka tulostaa Hello, world!

Tallenna tiedosto ja palaa terminaali-ikkunaan ~/projects/hello_world-kansiossa. Linuxissa tai macOS:ssä kirjoita seuraavat komennot tiedoston kääntämiseksi ja suorittamiseksi:

$ rustc main.rs
$ ./main
Hello, world!

Windowsissa kirjoita komento .\main ./main-komennon sijaan:

> rustc main.rs
> .\main
Hello, world!

Käyttöjärjestelmästä riippumatta merkkijono Hello, world! pitäisi tulostua terminaaliin. Jos et näe tätä tulostetta, katso Vianmääritys -osio asennusluvusta saadaksesi apua.

Jos Hello, world! tulostui, onnittelut! Olet virallisesti kirjoittanut Rust-ohjelman. Se tekee sinusta Rust-ohjelmoijan—tervetuloa!

Rust-ohjelman rakenne

Käydään tämä “Hello, world!” -ohjelma läpi yksityiskohtaisesti. Tässä on ensimmäinen palanen:

fn main() {

}

Nämä rivit määrittelevät funktion nimeltä main. main-funktio on erityinen: se on aina ensimmäinen koodi, joka suoritetaan jokaisessa ajettavassa Rust-ohjelmassa. Tässä ensimmäinen rivi julistaa main-nimisen funktion, jolla ei ole parametreja eikä se palauta mitään. Jos parametreja olisi, ne olisivat sulkeissa (()).

Funktion runko on kääritty {}-merkkeihin. Rust vaatii aaltosulkeet kaikkien funktioiden runkojen ympärille. On hyvä tyyli sijoittaa avaava aaltosulje samalle riville funktion julistuksen kanssa ja jättää niiden väliin yksi välilyönti.

Huom: Jos haluat noudattaa yhtenäistä tyyliä Rust-projekteissa, voit käyttää automaattista muotoilutyökalua nimeltä rustfmt muotoillaksesi koodisi tietyssä tyylissä (lisätietoa rustfmt-työkalusta Liitteessä D). Rust-tiimi on sisällyttänyt tämän työkalun standardiin Rust-jakeluun, kuten rustc-kääntäjäkin, joten sen pitäisi olla jo asennettuna tietokoneellesi!

main-funktion rungossa on seuraava koodi:

#![allow(unused)]
fn main() {
println!("Hello, world!");
}

Tämä rivi tekee kaiken työn tässä pienessä ohjelmassa: se tulostaa tekstiä näytölle. Tässä on kolme tärkeää yksityiskohtaa.

Ensinnäkin println! kutsuu Rust-makroa. Jos se olisi kutsunut funktiota, se kirjoitettaisiin println (ilman !-merkkiä). Rust-makrot ovat tapa kirjoittaa koodia, joka generoi koodia laajentaakseen Rustin syntaksia, ja käsittelemme niitä tarkemmin Luvussa 20. Toistaiseksi sinun tarvitsee vain tietää, että !-merkin käyttö tarkoittaa, että kutsut makroa tavallisen funktion sijaan ja että makrot eivät aina noudata samoja sääntöjä kuin funktiot.

Toiseksi näet merkkijonon "Hello, world!". Välitämme tämän merkkijonon argumenttina println!-makrolle, ja merkkijono tulostetaan näytölle.

Kolmanneksi päätteemme rivin puolipisteellä (;), joka osoittaa, että tämä lauseke on ohi ja seuraava on valmis alkamaan. Useimmat Rust-koodin rivit päättyvät puolipisteeseen.

Kääntäminen ja suorittaminen

Suoritit juuri juuri luomasi ohjelman, joten tarkastellaan prosessin jokaista vaihetta.

Ennen Rust-ohjelman suorittamista sinun on käännettävä se Rust-kääntäjällä kirjoittamalla rustc-komento ja välittämällä sille lähdekooditiedostosi nimi, näin:

$ rustc main.rs

Jos sinulla on C- tai C++-tausta, huomaat, että tämä on samankaltaista kuin gcc tai clang. Onnistuneen kääntämisen jälkeen Rust tuottaa binäärisen ajettavan tiedoston.

Linuxissa, macOS:ssä ja PowerShellissä Windowsilla näet ajettavan tiedoston kirjoittamalla ls-komennon komentorivilläsi:

$ ls
main  main.rs

Linuxissa ja macOS:ssä näet kaksi tiedostoa. PowerShellissä Windowsilla näet samat kolme tiedostoa kuin CMD:ssä. CMD:ssä Windowsilla kirjoittaisit seuraavan:

> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs

Tämä näyttää lähdekooditiedoston .rs-päätteellä, ajettavan tiedoston (main.exe Windowsilla, mutta main kaikilla muilla alustoilla) ja Windowsilla käytettäessä tiedoston, joka sisältää virheenkorjaustietoja .pdb-päätteellä. Tästä eteenpäin suoritat main- tai main.exe-tiedoston näin:

$ ./main # or .\main on Windows

Jos main.rs on “Hello, world!” -ohjelmasi, tämä rivi tulostaa Hello, world! terminaaliisi.

Jos olet tottunut dynaamiseen kieleen, kuten Rubyyn, Pythoniin tai JavaScriptiin, et ehkä ole tottunut kääntämisen ja ohjelman suorittamisen erottamiseen eri vaiheiksi. Rust on etukäteen käännettävä kieli, mikä tarkoittaa, että voit kääntää ohjelman ja antaa ajettavan tiedoston jollekin toiselle, ja he voivat suorittaa sen vaikka heillä ei olisi Rustia asennettuna. Jos annat jollekulle .rb-, .py- tai .js-tiedoston, he tarvitsevat Ruby-, Python- tai JavaScript-toteutuksen asennettuna (vastaavasti). Mutta näissä kielissä tarvitset vain yhden komennon ohjelmasi kääntämiseen ja suorittamiseen. Kaikki on kompromissi kielen suunnittelussa.

Pelkkä kääntäminen rustc-kääntäjällä riittää yksinkertaisiin ohjelmiin, mutta projektisi kasvaessa haluat hallita kaikkia vaihtoehtoja ja helpottaa koodisi jakamista. Seuraavaksi esittelemme sinulle Cargo-työkalun, joka auttaa kirjoittamaan tosielämän Rust-ohjelmia.

Hei, Cargo!

Hello, Cargo!

Cargo on Rustin build-järjestelmä ja paketinhallinta. Useimmat Rustaceanit käyttävät tätä työkalua Rust-projektiensa hallintaan, koska Cargo hoitaa puolestasi monia tehtäviä, kuten koodisi kääntämisen, koodisi tarvitsemien kirjastojen lataamisen ja näiden kirjastojen kääntämisen. (Kutsumme koodisi tarvitsemia kirjastoja riippuvuuksiksi.)

Yksinkertaisimmilla Rust-ohjelmilla, kuten tähän asti kirjoittamallamme, ei ole riippuvuuksia. Jos olisimme rakentaneet “Hello, world!” -projektin Cargolla, se käyttäisi vain Cargon osaa, joka hoitaa koodisi kääntämisen. Kun kirjoitat monimutkaisempia Rust-ohjelmia, lisäät riippuvuuksia, ja jos aloitat projektin Cargolla, riippuvuuksien lisääminen on paljon helpompaa.

Koska valtava enemmistö Rust-projekteista käyttää Cargo-työkalua, tämän kirjan loppuosa olettaa, että käytät sitä myös. Cargo tulee Rustin mukana, jos käytit virallisia asennusohjelmia, joita käsiteltiin Asennus -osiossa. Jos asensit Rustin jollain muulla tavalla, tarkista, onko Cargo asennettu kirjoittamalla terminaaliisi:

$ cargo --version

Jos näet versionumeron, sinulla on se! Jos näet virheen, kuten command not found, katso asennustapasi dokumentaatiosta, miten Cargo asennetaan erikseen.

Projektin luominen Cargolla

Luodaan uusi projekti Cargolla ja katsotaan, miten se eroaa alkuperäisestä “Hello, world!” -projektistamme. Palaa projects-kansioosi (tai minne päätit tallentaa koodisi). Suorita sitten millä tahansa käyttöjärjestelmällä seuraavat:

$ cargo new hello_cargo
$ cd hello_cargo

Ensimmäinen komento luo uuden kansion ja projektin nimeltä hello_cargo. Olemme nimenneet projektimme hello_cargo, ja Cargo luo tiedostonsa samannimiseen kansioon.

Siirry hello_cargo-kansioon ja listaa tiedostot. Näet, että Cargo on luonut meille kaksi tiedostoa ja yhden kansion: Cargo.toml-tiedoston ja src-kansion, jonka sisällä on main.rs-tiedosto.

Se on myös alustanut uuden Git-repositorion .gitignore-tiedoston kera. Git-tiedostoja ei luoda, jos suoritat cargo new olemassa olevassa Git-repositoriossa; voit ohittaa tämän käyttämällä cargo new --vcs=git.

Huom: Git on yleinen versionhallintajärjestelmä. Voit muuttaa cargo new -komennon käyttämään eri versionhallintajärjestelmää tai ei lainkaan versionhallintaa --vcs-lipulla. Suorita cargo new --help nähdäksesi käytettävissä olevat vaihtoehdot.

Avaa Cargo.toml valitsemassasi tekstieditorissa. Sen pitäisi näyttää samankaltaiselta kuin Listauksen 1-2 koodi.

Filename: Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"

[dependencies]
Listing 1-2: cargo new -komennon luoman Cargo.toml-tiedoston sisältö

Tämä tiedosto on TOML (Tom’s Obvious, Minimal Language) -muodossa, joka on Cargon konfiguraatiomuoto.

Ensimmäinen rivi [package] on osion otsikko, joka osoittaa, että seuraavat lausekkeet konfiguroivat pakettia. Kun lisäämme tähän tiedostoon lisätietoa, lisäämme muita osioita.

Seuraavat kolme riviä asettavat konfiguraatiotiedot, joita Cargo tarvitsee ohjelmasi kääntämiseen: nimen, version ja käytettävän Rust-editionin. Puhumme edition-avaimesta Liitteessä E.

Viimeinen rivi [dependencies] on osion alku, johon voit listata projektisi riippuvuudet. Rustissa koodipaketteja kutsutaan crateiksi. Emme tarvitse muita crateja tähän projektiin, mutta tarvitsemme Luvun 2 ensimmäisessä projektissa, joten käytämme tätä riippuvuuksien osiota silloin.

Avaa nyt src/main.rs ja katso:

Filename: src/main.rs

fn main() {
    println!("Hello, world!");
}

Cargo on luonut sinulle “Hello, world!” -ohjelman, aivan kuten Listauksessa 1-1 kirjoittamamme! Tähän asti erot projektimme ja Cargon luoman projektin välillä ovat, että Cargo sijoitti koodin src-kansioon ja meillä on Cargo.toml-konfiguraatiotiedosto ylätason kansiossa.

Cargo odottaa lähdekooditiedostojesi olevan src-kansiossa. Ylätason projektikansio on vain README-tiedostoille, lisenssitiedoille, konfiguraatiotiedostoille ja kaikelle muulle, mikä ei liity koodiisi. Cargon käyttö auttaa järjestämään projektisi. Kaikella on paikkansa, ja kaikki on paikallaan.

Jos aloitit projektin, joka ei käytä Cargo-työkalua, kuten teimme “Hello, world!” -projektissa, voit muuntaa sen Cargoa käyttäväksi projektiksi. Siirrä projektikoodi src-kansioon ja luo sopiva Cargo.toml-tiedosto. Helppo tapa saada Cargo.toml-tiedosto on suorittaa cargo init, joka luo sen automaattisesti.

Cargo-projektin kääntäminen ja suorittaminen

Katsotaan nyt, mikä on erilaista, kun käännetään ja suoritetaan “Hello, world!” -ohjelma Cargolla! hello_cargo-kansiostasi käänä projektisi kirjoittamalla seuraava komento:

$ cargo build
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs

Tämä komento luo ajettavan tiedoston target/debug/hello_cargo (tai target\debug\hello_cargo.exe Windowsilla) nykyisen kansiosi sijaan. Koska oletuskäännös on debug-käännös, Cargo sijoittaa binäärin kansioon nimeltä debug. Voit suorittaa ajettavan tiedoston tällä komennolla:

$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
Hello, world!

Jos kaikki menee hyvin, Hello, world! pitäisi tulostua terminaaliin. Ensimmäinen cargo build -komennon suoritus saa myös Cargon luomaan uuden tiedoston ylätasolle: Cargo.lock. Tämä tiedosto pitää kirjaa projektisi riippuvuuksien tarkoista versioista. Tällä projektilla ei ole riippuvuuksia, joten tiedosto on hieman niukka. Sinun ei koskaan tarvitse muuttaa tätä tiedostoa manuaalisesti; Cargo hallitsee sen sisältöä puolestasi.

Käänsimme juuri projektin cargo build -komennolla ja suoritimme sen ./target/debug/hello_cargo -komennolla, mutta voimme myös käyttää cargo run -komentoa kääntääksemme koodin ja suorittaaksemme syntyneen ajettavan tiedoston yhdellä komennolla:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/hello_cargo`
Hello, world!

cargo run -komennon käyttö on kätevämpää kuin muistaa suorittaa cargo build ja käyttää sitten koko polkua binääriin, joten useimmat kehittäjät käyttävät cargo run -komentoa.

Huomaa, että tällä kertaa emme nähneet tulostetta, joka osoittaa Cargon kääntävän hello_cargo-projektia. Cargo päätteli, että tiedostot eivät olleet muuttuneet, joten se ei kääntänyt uudelleen vaan suoritti vain binäärin. Jos olisit muokannut lähdekoodiasi, Cargo olisi kääntänyt projektin uudelleen ennen suorittamista, ja olisit nähnyt tämän tulosteen:

$ cargo run
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
     Running `target/debug/hello_cargo`
Hello, world!

Cargo tarjoaa myös komennon nimeltä cargo check. Tämä komento tarkistaa nopeasti koodisi varmistaakseen, että se kääntyy, mutta ei tuota ajettavaa tiedostoa:

$ cargo check
   Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs

Miksi et haluaisi ajettavaa tiedostoa? Usein cargo check on paljon nopeampi kuin cargo build, koska se ohittaa ajettavan tiedoston tuottamisen. Jos tarkistat työtäsi jatkuvasti koodia kirjoittaessasi, cargo check -komennon käyttö nopeuttaa prosessia, jolla saat tietää, kääntyykö projektisi edelleen! Siksi monet Rustaceanit suorittavat cargo check -komennon säännöllisesti ohjelmaa kirjoittaessaan varmistaakseen, että se kääntyy. Sitten he suorittavat cargo build -komennon, kun ovat valmiita käyttämään ajettavaa tiedostoa.

Kerrataan, mitä olemme tähän mennessä oppineet Cargosta:

  • Voimme luoda projektin cargo new -komennolla.
  • Voimme kääntää projektin cargo build -komennolla.
  • Voimme kääntää ja suorittaa projektin yhdellä askeleella cargo run -komennolla.
  • Voimme kääntää projektin tuottamatta binääriä virheiden tarkistamiseksi cargo check -komennolla.
  • Sen sijaan, että tallentaisimme käännöstuloksen samaan kansioon kuin koodimme, Cargo säilyttää sen target/debug-kansiossa.

Cargon käytön lisäetu on, että komennot ovat samat riippumatta siitä, millä käyttöjärjestelmällä työskentelet. Tästä eteenpäin emme enää anna erityisiä ohjeita Linuxille ja macOS:lle verrattuna Windowsiin.

Julkaisua varten kääntäminen

Kun projektisi on vihdoin valmis julkaistavaksi, voit käyttää cargo build --release -komentoa kääntääksesi sen optimoinneilla. Tämä komento luo ajettavan tiedoston target/release-kansioon target/debug-kansion sijaan. Optimoinnit tekevät Rust-koodistasi nopeammin suoritettavan, mutta niiden käyttöönotto pidentää ohjelmasi kääntämiseen kuluvaa aikaa. Siksi on kaksi eri profiilia: yksi kehitystä varten, kun haluat kääntää nopeasti ja usein, ja toinen lopullisen ohjelman rakentamiseen, jota et rakenna uudelleen toistuvasti ja joka suoritetaan mahdollisimman nopeasti. Jos vertailet koodisi suoritusaikaa, muista suorittaa cargo build --release ja vertailla target/release-kansion ajettavalla tiedostolla.

Cargon käytäntöjen hyödyntäminen

Yksinkertaisissa projekteissa Cargo ei tarjoa paljon lisäarvoa pelkän rustc-kääntäjän käyttöön verrattuna, mutta se osoittaa arvonsa ohjelmiesi monimutkaistuessa. Kun ohjelmat kasvavat useiksi tiedostoiksi tai tarvitsevat riippuvuuden, on paljon helpompaa antaa Cargon koordinoida käännös.

Vaikka hello_cargo-projekti on yksinkertainen, se käyttää nyt suurta osaa tosielämän työkaluista, joita käytät Rust-urasi loppuosassa. Itse asiassa voit työskennellä olemassa olevien projektien parissa käyttämällä seuraavia komentoja koodin hakemiseen Gitillä, siirtymiseen projektin kansioon ja kääntämiseen:

$ git clone example.org/someproject
$ cd someproject
$ cargo build

Lisätietoa Cargosta löydät sen dokumentaatiosta.

Yhteenveto

Olet jo päässyt hyvään alkuun Rust-matkallasi! Tässä luvussa opit:

  • Rustin uusimman vakaan version asentamisen rustup-työkalulla.
  • Päivittämisen uudempaan Rust-versioon.
  • Paikallisesti asennetun dokumentaation avaamisen.
  • “Hello, world!” -ohjelman kirjoittamisen ja suorittamisen suoraan rustc-kääntäjällä.
  • Uuden projektin luomisen ja suorittamisen Cargon käytäntöjen mukaisesti.

Nyt on hyvä aika rakentaa laajempi ohjelma totuttuaksesi Rust-koodin lukemiseen ja kirjoittamiseen. Luvussa 2 rakennamme arvauspeli-ohjelman. Jos haluat mieluummin aloittaa oppimalla, miten yleiset ohjelmointikäsitteet toimivat Rustissa, katso Luku 3 ja palaa sitten Lukuun 2.

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.

Yleiset ohjelmointikäsitteet

Tämä luku käsittelee käsitteitä, jotka esiintyvät lähes jokaisessa ohjelmointikielessä, ja sitä, miten ne toimivat Rustissa. Monilla ohjelmointikielillä on paljon yhteistä ytimessään. Mikään tässä luvussa esitelty käsite ei ole ainutlaatuinen Rustille, mutta käsittelemme ne Rustin kontekstissa ja selitämme niiden käyttöön liittyvät käytännöt.

Erityisesti opit muuttujista, perustyypeistä, funktioista, kommenteista ja ohjausrakenteista. Nämä perusteet ovat jokaisessa Rust-ohjelmassa, ja niiden oppiminen varhain antaa sinulle vahvan ytimen, josta lähteä.

Avainsanat

Rust-kielellä on joukko avainsanoja, jotka on varattu vain kielen käyttöön, kuten muissakin kielissä. Muista, että et voi käyttää näitä sanoja muuttujien tai funktioiden niminä. Useimmilla avainsanoilla on erityisiä merkityksiä, ja käytät niitä erilaisiin tehtäviin Rust-ohjelmissasi; muutamilla ei ole tällä hetkellä liitettyä toiminnallisuutta, mutta ne on varattu toiminnallisuudelle, joka saatetaan lisätä Rustiin tulevaisuudessa. Löydät avainsanojen listan Liitteestä A.

Muuttujat ja muuttumattomuus

Muuttujat ja muuttuvuus

Kuten mainittiin “Arvojen tallentaminen muuttujilla” -osiossa, muuttujat ovat oletuksena muuttumattomia. Tämä on yksi monista Rustin antamista vihjeistä kirjoittaa koodia tavalla, joka hyödyntää Rustin tarjoamaa turvallisuutta ja helppoa rinnakkaisuutta. Sinulla on kuitenkin edelleen mahdollisuus tehdä muuttujistasi muuttuvia. Tutkitaan, miten ja miksi Rust kannustaa suosimaan muuttumattomuutta ja miksi joskus saatat haluta poiketa siitä.

Kun muuttuja on muuttumaton, kun arvo on sidottu nimeen, et voi muuttaa sitä arvoa. Havainnollistamiseksi luo uusi projekti nimeltä variables projects-kansioosi komennolla cargo new variables.

Avaa sitten uudessa variables-kansiossasi src/main.rs ja korvaa sen koodi seuraavalla koodilla, joka ei vielä käänny:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-01-variables-are-immutable/src/main.rs}}

Tallenna ja suorita ohjelma cargo run -komennolla. Sinun pitäisi saada virheilmoitus muuttumattomuusvirheestä, kuten tässä tulosteessa:

{{#include ../listings/ch03-common-programming-concepts/no-listing-01-variables-are-immutable/output.txt}}

Tämä esimerkki näyttää, miten kääntäjä auttaa löytämään virheitä ohjelmissasi. Kääntäjävirheet voivat olla turhauttavia, mutta todellisuudessa ne vain tarkoittavat, että ohjelmasi ei vielä turvallisesti tee sitä, mitä haluat sen tekevän; ne eivät tarkoita, ettet olisi hyvä ohjelmoija! Kokeneetkin Rustaceanit saavat kääntäjävirheitä.

Sait virheilmoituksen cannot assign twice to immutable variable `x` koska yritit antaa toisen arvon muuttumattomalle x-muuttujalle.

On tärkeää, että saamme käännösaikaisia virheitä, kun yritämme muuttaa arvoa, joka on merkitty muuttumattomaksi, koska juuri tämä tilanne voi johtaa bugeihin. Jos yksi osa koodistamme toimii olettaen, että arvo ei koskaan muutu, ja toinen osa koodistamme muuttaa sitä arvoa, on mahdollista, että koodin ensimmäinen osa ei tee sitä, mitä se oli suunniteltu tekemään. Tällaisen bugin syy voi olla vaikea jäljittää jälkikäteen, erityisesti kun koodin toinen osa muuttaa arvoa vain joskus. Rust-kääntäjä takaa, että kun ilmoitat arvon olevan muuttumaton, se todella ei muutu, joten sinun ei tarvitse seurata sitä itse. Koodisi on siten helpompi ymmärtää.

Mutta muuttuvuus voi olla hyvin hyödyllistä ja tehdä koodin kirjoittamisesta kätevämpää. Vaikka muuttujat ovat oletuksena muuttumattomia, voit tehdä niistä muuttuvia lisäämällä mut-avainsanan muuttujan nimen eteen, kuten teit Luvussa 2. mut-avainsanan lisääminen myös välittää aikomuksen koodin tuleville lukijoille osoittaen, että koodin muut osat muuttavat tämän muuttujan arvoa.

Muutetaan esimerkiksi src/main.rs seuraavaksi:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-02-adding-mut/src/main.rs}}
}

Kun suoritamme ohjelman nyt, saamme tämän:

{{#include ../listings/ch03-common-programming-concepts/no-listing-02-adding-mut/output.txt}}

Saamme muuttaa x:ään sidottua arvoa 5:stä 6:een, kun mut on käytössä. Lopulta päätös muuttuvuuden käytöstä on sinun ja riippuu siitä, mikä on selkeintä kyseisessä tilanteessa.

Vakioiden julistaminen

Kuten muuttumattomat muuttujat, vakiot ovat arvoja, jotka on sidottu nimeen eikä niitä saa muuttaa, mutta vakioiden ja muuttujien välillä on muutamia eroja.

Ensinnäkin et saa käyttää mut-avainsanaa vakioiden kanssa. Vakiot eivät ole vain oletuksena muuttumattomia—ne ovat aina muuttumattomia. Julistat vakiot const-avainsanalla let-avainsanan sijaan, ja arvon tyyppi täytyy annotoida. Käsittelemme tyyppejä ja tyyppiannotaatioita seuraavassa osiossa “Tietotyypit”, joten älä huoli yksityiskohdista vielä. Tiedä vain, että sinun täytyy aina annotoida tyyppi.

Vakioita voidaan julistaa missä tahansa näkyvyysalueessa, mukaan lukien globaalissa näkyvyysalueessa, mikä tekee niistä hyödyllisiä arvoille, jotka monien koodin osien tarvitsevat tietää.

Viimeinen ero on, että vakioille voidaan asettaa vain vakiolauseke, ei arvon tulos, joka voitaisiin laskea vain ajonaikana.

Tässä on esimerkki vakion julistuksesta:

#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

Vakion nimi on THREE_HOURS_IN_SECONDS, ja sen arvo on 60 (sekuntien määrä minuutissa) kerrottuna 60:llä (minuuttien määrä tunnissa) kerrottuna 3:lla (tuntien määrä, jonka haluamme laskea tässä ohjelmassa). Rustin nimeämiskäytäntö vakioille on käyttää kaikkia kirjaimia isoina ja alaviivoja sanojen välissä. Kääntäjä pystyy arvioimaan rajoitetun joukon operaatioita käännösaikana, mikä antaa meille mahdollisuuden kirjoittaa tämän arvon tavalla, joka on helpompi ymmärtää ja tarkistaa, sen sijaan että asettaisimme vakion arvoksi 10 800. Katso Rust Referencen osio vakioiden arvioinnista lisätietoja siitä, mitä operaatioita voidaan käyttää vakioita julistaessa.

Vakiot ovat voimassa koko ohjelman ajon ajan niiden julistusalueella. Tämä ominaisuus tekee vakioista hyödyllisiä sovellusalueesi arvoille, jotka useat ohjelman osat saattavat tarvita tietää, kuten maksimipisteet, jotka mikä tahansa pelaaja voi ansaita, tai valon nopeus.

Kovakoodattujen arvojen nimeäminen vakioiksi koko ohjelmassasi on hyödyllistä välittääkseen arvon merkityksen koodin tuleville ylläpitäjille. Se auttaa myös sillä, että sinun tarvitsee muuttaa vain yhtä paikkaa koodissasi, jos kovakoodattua arvoa pitää päivittää tulevaisuudessa.

Varjostaminen

Kuten näit arvauspeli-oppaassa Luvussa 2, voit julistaa uuden muuttujan samalla nimellä kuin aiempi muuttuja. Rustaceanit sanovat, että ensimmäinen muuttuja varjostetaan toisella, mikä tarkoittaa, että toinen muuttuja on se, jonka kääntäjä näkee, kun käytät muuttujan nimeä. Käytännössä toinen muuttuja peittää ensimmäisen, ohjaten kaikki muuttujan nimen käytöt itseensä, kunnes se itse varjostetaan tai näkyvyysalue päättyy. Voimme varjostaa muuttujan käyttämällä samaa muuttujan nimeä ja toistamalla let-avainsanan käytön seuraavasti:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-03-shadowing/src/main.rs}}
}

Tämä ohjelma sitoo ensin x:n arvoon 5. Sitten se luo uuden muuttujan x toistamalla let x =, ottaen alkuperäisen arvon ja lisäten 1, jolloin x:n arvo on 6. Sitten sisäisessä näkyvyysalueessa, joka luotiin aaltosulkeilla, kolmas let-lauseke varjostaa myös x:n ja luo uuden muuttujan, kertomalla edellisen arvon kahdella antaakseen x:lle arvon 12. Kun tämä näkyvyysalue päättyy, sisäinen varjostaminen loppuu ja x palaa arvoon 6. Kun suoritamme tämän ohjelman, se tulostaa seuraavan:

{{#include ../listings/ch03-common-programming-concepts/no-listing-03-shadowing/output.txt}}

Varjostaminen eroaa muuttujan merkitsemisestä mut-avainsanalla, koska saamme käännösaikaisen virheen, jos yritämme vahingossa antaa uuden arvon tälle muuttujalle käyttämättä let-avainsanaa. Käyttämällä let-avainsanaa voimme suorittaa muutamia muunnoksia arvolle, mutta muuttuja on muuttumaton näiden muunnosten jälkeen.

Toinen ero mut-avainsanan ja varjostamisen välillä on, että koska luomme käytännössä uuden muuttujan, kun käytämme let-avainsanaa uudelleen, voimme muuttaa arvon tyyppiä mutta käyttää samaa nimeä. Esimerkiksi ohjelmamme saattaa pyytää käyttäjää näyttämään, kuinka monta välilyöntiä hän haluaa tekstin väliin syöttämällä välilyöntimerkkejä, ja sitten haluamme tallentaa tämän syötteen numerona:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-04-shadowing-can-change-types/src/main.rs:here}}
}

Ensimmäinen spaces-muuttuja on merkkijonotyyppiä, ja toinen spaces-muuttuja on numerotyyppiä. Varjostaminen säästää meidät keksimästä eri nimiä, kuten spaces_str ja spaces_num; sen sijaan voimme käyttää uudelleen yksinkertaisempaa spaces-nimeä. Jos kuitenkin yritämme käyttää mut-avainsanaa tähän, kuten tässä näytetään, saamme käännösaikaisen virheen:

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-05-mut-cant-change-types/src/main.rs:here}}

Virhe sanoo, ettemme saa muuttaa muuttujan tyyppiä:

{{#include ../listings/ch03-common-programming-concepts/no-listing-05-mut-cant-change-types/output.txt}}

Nyt kun olemme tutkineet, miten muuttujat toimivat, katsotaan lisää tietotyyppejä, joita niillä voi olla.

Tietotyypit

Tietotyypit

Jokainen arvo Rustissa on tiettyä tietotyyppiä, joka kertoo Rustille, millaista dataa määritellään, jotta se tietää, miten datan kanssa työskennellään. Tarkastelemme kahta tietotyyppien osajoukkoa: skalaareja ja yhdistelmiä.

Muista, että Rust on staattisesti tyypitetty kieli, mikä tarkoittaa, että sen täytyy tietää kaikkien muuttujien tyypit käännösaikana. Kääntäjä pystyy yleensä päättelemään, mitä tyyppiä haluamme käyttää arvon ja sen käytön perusteella. Tilanteissa, joissa useita tyyppejä on mahdollisia, kuten kun muunnimme String-tyypin numerotyypiksi parse-funktiolla “Arvauksen vertaaminen salaisnumeroon” -osiossa Luvussa 2, meidän täytyy lisätä tyyppiannotaatio, kuten tässä:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

Jos emme lisää edellä olevan koodin : u32 -tyyppiannotaatiota, Rust näyttää seuraavan virheen, mikä tarkoittaa, että kääntäjä tarvitsee meiltä lisätietoa tietääkseen, mitä tyyppiä haluamme käyttää:

{{#include ../listings/ch03-common-programming-concepts/output-only-01-no-type-annotations/output.txt}}

Näet erilaisia tyyppiannotaatioita muille tietotyypeille.

Skalaarityypit

Skalaari-tyyppi edustaa yksittäistä arvoa. Rustissa on neljä pääasiallista skalaarityyppiä: kokonaisluvut, liukuluvut, totuusarvot ja merkit. Saatat tunnistaa nämä muista ohjelmointikielistä. Katsotaan, miten ne toimivat Rustissa.

Kokonaislukutyypit

Kokonaisluku on luku ilman murto-osaa. Käytimme yhtä kokonaislukutyyppiä Luvussa 2, u32-tyyppiä. Tämä tyyppijulistus osoittaa, että siihen liitetyn arvon pitäisi olla etumerkitön kokonaisluku (etumerkityt kokonaislukutyypit alkavat i:llä u:n sijaan), joka vie 32 bittiä tilaa. Taulukko 3-1 näyttää Rustin sisäänrakennetut kokonaislukutyypit. Voimme käyttää mitä tahansa näistä muunnoksista kokonaislukuarvon tyypin julistamiseen.

Taulukko 3-1: Kokonaislukutyypit Rustissa

PituusEtumerkittyEtumerkitön
8-bittineni8u8
16-bittineni16u16
32-bittineni32u32
64-bittineni64u64
128-bittineni128u128
Arkkitehtuurista riippuvaisizeusize

Jokainen muunnos voi olla joko etumerkitty tai etumerkitön ja sillä on eksplisiittinen koko. Etumerkitty ja etumerkitön viittaavat siihen, voiko luku olla negatiivinen—toisin sanoen, tarvitseeko luvulla olla etumerkki (etumerkitty) vai onko se aina positiivinen ja voidaan siten esittää ilman etumerkkiä (etumerkitön). Se on kuin lukujen kirjoittaminen paperille: Kun etumerkillä on merkitystä, luku näytetään plus- tai miinusmerkillä; kun on turvallista olettaa luvun olevan positiivinen, sitä ei näytetä etumerkillä. Etumerkityt luvut tallennetaan kahden komplementin esityksellä.

Jokainen etumerkitty muunnos voi tallentaa lukuja väliltä −(2n − 1) – 2n − 1 − 1, missä n on kyseisen muunnoksen käyttämien bittien määrä. Eli i8 voi tallentaa lukuja −(27) – 27 − 1, mikä vastaa −128 – 127. Etumerkittömät muunnokset voivat tallentaa lukuja 0 – 2n − 1, joten u8 voi tallentaa lukuja 0 – 28 − 1, mikä vastaa 0 – 255.

Lisäksi isize- ja usize-tyypit riippuvat tietokoneen arkkitehtuurista, jolla ohjelmasi suoritetaan: 64 bittiä 64-bittisellä arkkitehtuurilla ja 32 bittiä 32-bittisellä arkkitehtuurilla.

Voit kirjoittaa kokonaislukuliteraaleja missä tahansa Taulukossa 3-2 näytetyssä muodossa. Huomaa, että lukuliteraaleilla, jotka voivat olla useita numeerisia tyyppejä, voi olla tyyppiliite, kuten 57u8, tyypin määrittämiseksi. Lukuliteraalit voivat myös käyttää _-merkkiä visuaalisen erottimen roolissa lukujen lukemisen helpottamiseksi, kuten 1_000, jolla on sama arvo kuin 1000:lla.

Taulukko 3-2: Kokonaislukuliteraalit Rustissa

LukuliteraalitEsimerkki
Desimaali98_222
Heksadesimaali0xff
Oktaaliluku0o77
Binääri0b1111_0000
Tavu (u8 vain)b'A'

Mistä siis tiedät, mitä kokonaislukutyyppiä käyttää? Jos et ole varma, Rustin oletusarvot ovat yleensä hyvä lähtökohta: Kokonaislukutyypit oletuksena i32. Pääasiallinen tilanne, jossa käyttäisit isize- tai usize-tyyppiä, on kun indeksoit jotakin kokoelmaa.

Kokonaisluvun ylivuoto

Sanotaan, että sinulla on u8-tyyppinen muuttuja, joka voi pitää arvoja 0 – 255. Jos yrität muuttaa muuttujan arvoksi jotain tämän alueen ulkopuolelta, kuten 256, tapahtuu kokonaisluvun ylivuoto, mikä voi johtaa kahteen käyttäytymiseen. Kun käännetään debug-tilassa, Rust sisällyttää kokonaisluvun ylivuodon tarkistukset, jotka saavat ohjelmasi panikoimaan ajonaikana, jos tämä tapahtuu. Rust käyttää termiä panikointi, kun ohjelma päättyy virheeseen; käsittelemme paniikkeja tarkemmin “Palautumattomat virheet panic!-makrolla” -osiossa Luvussa 9.

Kun käännetään release-tilassa --release-lipulla, Rust ei sisällytä kokonaisluvun ylivuodon tarkistuksia, jotka aiheuttavat paniikin. Sen sijaan, jos ylivuoto tapahtuu, Rust suorittaa kahden komplementin kiertämisen. Lyhyesti sanottuna tyypin maksimiarvoa suuremmat arvot “kiertyvät” tyypin vähimmäisarvoon. u8-tapauksessa arvo 256 muuttuu 0:ksi, arvo 257 muuttuu 1:ksi ja niin edelleen. Ohjelma ei panikoi, mutta muuttujalla on arvo, joka ei todennäköisesti ole se, mitä odotit. Kokonaisluvun ylivuodon kiertämiskäyttäytymiseen luottaminen katsotaan virheeksi.

Käsitelläksesi ylivuodon mahdollisuuden eksplisiittisesti voit käyttää standardikirjaston tarjoamia metodiperheitä primitiivisille numeerisille tyypeille:

  • Kierrä kaikissa käännöstiloissa wrapping_*-metodeilla, kuten wrapping_add.
  • Palauta None-arvo, jos ylivuotoa tapahtuu checked_*-metodeilla.
  • Palauta arvo ja totuusarvo, joka osoittaa, tapahtuiko ylivuotoa overflowing_*-metodeilla.
  • Rajoita arvon minimi- tai maksimiarvoihin saturating_*-metodeilla.

Liukulukutyypit

Rustissa on myös kaksi primitiivistä liukulukutyyppiä, jotka ovat lukuja desimaalipisteellä. Rustin liukulukutyypit ovat f32 ja f64, joiden koot ovat 32 ja 64 bittiä. Oletustyyppi on f64, koska nykyaikaisilla prosessoreilla se on suunnilleen yhtä nopea kuin f32, mutta tarjoaa enemmän tarkkuutta. Kaikki liukulukutyypit ovat etumerkittyjä.

Tässä on esimerkki liukuluvuista toiminnassa:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-06-floating-point/src/main.rs}}
}

Liukuluvut esitetään IEEE-754-standardin mukaisesti.

Numeeriset operaatiot

Rust tukee perusmatemaattisia operaatioita, joita odotat kaikille lukutyypeille: yhteenlasku, vähennyslasku, kertolasku, jakolasku ja jakojäännös. Kokonaislukujako katkaisee kohti nollaa lähimpään kokonaislukuun. Seuraava koodi näyttää, miten käyttäisit kutakin numeerista operaatiota let-lausekkeessa:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-07-numeric-operations/src/main.rs}}
}

Jokainen näiden lausekkeiden lauseke käyttää matemaattista operaattoria ja evaluoituu yhdeksi arvoksi, joka sidotaan sitten muuttujaan. Liite B sisältää listan kaikista Rustin tarjoamista operaattoreista.

Totuusarvotyyppi

Kuten useimmissa muissa ohjelmointikielissä, totuusarvotyypillä Rustissa on kaksi mahdollista arvoa: true ja false. Totuusarvot ovat yhden tavun kokoisia. Rustin totuusarvotyyppi määritellään bool-avainsanalla. Esimerkiksi:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-08-boolean/src/main.rs}}
}

Pääasiallinen tapa käyttää totuusarvoja on ehtolausekkeiden kautta, kuten if-lauseke. Käsittelemme, miten if-lausekkeet toimivat Rustissa “Ohjausrakenteet” -osiossa.

Merkkityyppi

Rustin char-tyyppi on kielen primitiivisin aakkosellinen tyyppi. Tässä on esimerkkejä char-arvojen julistamisesta:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-09-char/src/main.rs}}
}

Huomaa, että määrittelemme char-literaalit yksinkertaisilla lainausmerkeillä toisin kuin merkkijonoliteraalit, jotka käyttävät kaksinkertaisia lainausmerkkejä. Rustin char-tyyppi on 4 tavun kokoinen ja edustaa Unicode-skaalaariarvoa, mikä tarkoittaa, että se voi edustaa paljon enemmän kuin pelkkää ASCII:a. Aksenttimerkit; kiina-, japani- ja koreankieliset merkit; emojit ja nollaleveyden välilyönnit ovat kaikki kelvollisia char-arvoja Rustissa. Unicode-skaalaariarvot ovat välillä U+0000U+D7FF ja U+E000U+10FFFF. Unicode:ssa “merkki” ei kuitenkaan ole oikea käsite, joten intuitiosi siitä, mikä on “merkki”, ei välttämättä vastaa sitä, mikä on char Rustissa. Käsittelemme tämän aiheen tarkemmin “UTF-8-koodatun tekstin tallentaminen merkkijonoilla” -osiossa Luvussa 8.

Yhdistelmätyypit

Yhdistelmätyypit voivat ryhmitellä useita arvoja yhdeksi tyypiksi. Rustissa on kaksi primitiivistä yhdistelmätyyppiä: tuplat ja taulukot.

Tuplatyyppi

Tupla on yleinen tapa ryhmitellä useita erityyppisiä arvoja yhdeksi yhdistelmätyypiksi. Tuplalla on kiinteä pituus: kun se on julistettu, se ei voi kasvaa tai kutistua.

Luomme tuplan kirjoittamalla pilkuilla erotetun listan arvoista sulkeiden sisään. Jokaisella paikalla tuplassa on tyyppi, eivätkä tuplan eri arvojen tyypit tarvitse olla samoja. Olemme lisänneet valinnaiset tyyppiannotaatiot tähän esimerkkiin:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-10-tuples/src/main.rs}}
}

Muuttuja tup sitoo koko tuplan, koska tuplaa pidetään yhtenä yhdistelmäelementtinä. Saadaksemme yksittäiset arvot tuplasta voimme käyttää kuvioiden täsmäyttämistä tuplan purkamiseen, näin:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-11-destructuring-tuples/src/main.rs}}
}

Tämä ohjelma luo ensin tuplan ja sitoo sen muuttujaan tup. Sitten se käyttää kuviota let-avainsanan kanssa ottaakseen tup:n ja muuttaakseen sen kolmeksi erilliseksi muuttujaksi, x, y ja z. Tätä kutsutaan purkamiseksi, koska se jakaa yhden tuplan kolmeen osaan. Lopuksi ohjelma tulostaa y:n arvon, joka on 6.4.

Voimme myös käyttää tuplaelementtiä suoraan pisteellä (.) ja sen jälkeen sen arvon indeksillä, johon haluamme päästä. Esimerkiksi:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-12-tuple-indexing/src/main.rs}}
}

Tämä ohjelma luo tuplan x ja käyttää sitten kunkin tuplaelementin indeksiä. Kuten useimmissa ohjelmointikielissä, tuplan ensimmäinen indeksi on 0.

Tuplaa ilman arvoja kutsutaan erityisellä nimellä yksikkö. Tämä arvo ja sen vastaava tyyppi kirjoitetaan molemmat () ja edustavat tyhjää arvoa tai tyhjää paluutyyppiä. Lausekkeet palauttavat implisiittisesti yksikköarvon, jos ne eivät palauta mitään muuta arvoa.

Taulukkotyyppi

Toinen tapa kerätä useita arvoja on taulukko. Toisin kuin tuplassa, jokaisella taulukon elementillä täytyy olla sama tyyppi. Toisin kuin joissakin muissa kielissä, Rustin taulukoilla on kiinteä pituus.

Kirjoitamme taulukon arvot pilkuilla erotettuna listana hakasulkeiden sisään:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-13-arrays/src/main.rs}}
}

Taulukot ovat hyödyllisiä, kun haluat datasi allokoituvan pinolle, kuten muutkin tähän mennessä nähdyistä tyypeistä, eikä keolle (käsittelemme pinoa ja keon tarkemmin Luvussa 4), tai kun haluat varmistaa, että sinulla on aina kiinteä määrä elementtejä. Taulukko ei kuitenkaan ole yhtä joustava kuin vektorityyppi. Vektori on standardikirjaston tarjoama samankaltainen kokoelmatyyppi, jonka koko saa kasvaa tai kutistua, koska sen sisältö on keossa. Jos et ole varma, käytätkö taulukkoa vai vektoria, käytä todennäköisesti vektoria. Luku 8 käsittelee vektoreita tarkemmin.

Taulukot ovat kuitenkin hyödyllisempiä, kun tiedät, ettei elementtien määrän tarvitse muuttua. Esimerkiksi jos käyttäisit kuukausien nimiä ohjelmassa, käyttäisit todennäköisesti taulukkoa vektorin sijaan, koska tiedät sen sisältävän aina 12 elementtiä:

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

Kirjoitat taulukon tyypin hakasulkeilla, jokaisen elementin tyypillä, puolipisteellä ja sitten taulukon elementtien määrällä, näin:

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Tässä i32 on kunkin elementin tyyppi. Puolipisteen jälkeen luku 5 osoittaa, että taulukko sisältää viisi elementtiä.

Voit myös alustaa taulukon sisältämään saman arvon jokaiselle elementille määrittämällä alkuarvon, sitten puolipisteen ja taulukon pituuden hakasulkeissa, kuten tässä:

#![allow(unused)]
fn main() {
let a = [3; 5];
}

a-niminen taulukko sisältää 5 elementtiä, jotka kaikki asetetaan aluksi arvoon 3. Tämä on sama kuin kirjoittaisi let a = [3, 3, 3, 3, 3];, mutta tiiviimmällä tavalla.

Taulukkoelementtien käyttö

Taulukko on yksi muistilohko tunnetulla, kiinteällä koolla, joka voidaan allokoida pinolle. Voit käyttää taulukon elementtejä indeksoinnilla, näin:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-14-array-indexing/src/main.rs}}
}

Tässä esimerkissä first-niminen muuttuja saa arvon 1, koska se on taulukon indeksin [0] arvo. second-niminen muuttuja saa arvon 2 taulukon indeksistä [1].

Virheellinen taulukkoelementin käyttö

Katsotaan, mitä tapahtuu, jos yrität käyttää taulukon elementtiä taulukon lopun jälkeen. Sanotaan, että suoritat tämän koodin, samankaltaisena kuin arvauspeli Luvussa 2, saadaksesi taulukon indeksin käyttäjältä:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-15-invalid-array-access/src/main.rs}}

Tämä koodi kääntyy onnistuneesti. Jos suoritat tämän koodin cargo run -komennolla ja kirjoitat 0, 1, 2, 3 tai 4, ohjelma tulostaa vastaavan arvon taulukon kyseisessä indeksissä. Jos sen sijaan kirjoitat numeron taulukon lopun jälkeen, kuten 10, näet tulosteen kuten tämä:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Ohjelma johti ajonaikaiseen virheeseen virheellisen arvon käytössä indeksointioperatiossa. Ohjelma päättyi virheilmoitukseen eikä suorittanut viimeistä println!-lausetta. Kun yrität käyttää elementtiä indeksoinnilla, Rust tarkistaa, että määrittämäsi indeksi on pienempi kuin taulukon pituus. Jos indeksi on suurempi tai yhtä suuri kuin pituus, Rust panikoi. Tämä tarkistus täytyy tehdä ajonaikana, erityisesti tässä tapauksessa, koska kääntäjä ei voi mahdollisesti tietää, minkä arvon käyttäjä kirjoittaa, kun hän suorittaa koodin myöhemmin.

Tämä on esimerkki Rustin muistiturvallisuusperiaatteista käytännössä. Monissa matalan tason kielissä tällaista tarkistusta ei tehdä, ja kun annat virheellisen indeksin, virheelliseen muistiin voidaan päästä. Rust suojaa sinua tämänkaltaiselta virheeltä poistumalla välittömästi sen sijaan, että sallisi muistin käytön ja jatkuisi. Luku 9 käsittelee lisää Rustin virheenkäsittelystä ja siitä, miten voit kirjoittaa luettavaa, turvallista koodia, joka ei panikoi eikä salli virheellistä muistin käyttöä.

Funktiot

Funktiot

Funktiot ovat yleisiä Rust-koodissa. Olet jo nähnyt yhden tärkeimmistä kielen funktioista: main-funktion, joka on monien ohjelmien käynnistyspiste. Olet myös nähnyt fn-avainsanan, jonka avulla voit julistaa uusia funktioita.

Rust-koodi käyttää käärmeenpolku-tyyliä (snake case) funktioiden ja muuttujien nimien perinteisenä tyylinä, jossa kaikki kirjaimet ovat pieniä ja alaviivat erottavat sanat. Tässä on ohjelma, joka sisältää esimerkin funktion määrittelystä:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-16-functions/src/main.rs}}
}

Määrittelemme funktion Rustissa kirjoittamalla fn ja sen jälkeen funktion nimen ja joukon sulkeita. Aaltosulkeet kertovat kääntäjälle, missä funktion runko alkaa ja päättyy.

Voimme kutsua mitä tahansa määrittelemäämme funktiota kirjoittamalla sen nimen ja joukon sulkeita. Koska another_function on määritelty ohjelmassa, sitä voidaan kutsua main-funktion sisältä. Huomaa, että määrittelimme another_function-funktion main-funktion jälkeen lähdekoodissa; olisimme voineet määritellä sen ennenkin. Rust ei välitä, missä määrittelet funktiosi, vain siitä, että ne on määritelty jossakin näkyvyysalueessa, jonka kutsuja näkee.

Aloitetaan uusi binääriprojekti nimeltä functions tutkiaksemme funktioita tarkemmin. Sijoita another_function-esimerkki src/main.rs-tiedostoon ja suorita se. Sinun pitäisi nähdä seuraava tuloste:

{{#include ../listings/ch03-common-programming-concepts/no-listing-16-functions/output.txt}}

Rivit suoritetaan siinä järjestyksessä, jossa ne esiintyvät main-funktiossa. Ensin “Hello, world!” -viesti tulostetaan, sitten another_function kutsutaan ja sen viesti tulostetaan.

Parametrit

Voimme määritellä funktioita, joilla on parametreja, jotka ovat erityisiä muuttujia, jotka ovat osa funktion allekirjoitusta. Kun funktiolla on parametreja, voit antaa sille konkreettisia arvoja näille parametreille. Teknisesti konkreettisia arvoja kutsutaan argumenteiksi, mutta arkikeskusteluissa ihmiset käyttävät usein sanoja parametri ja argumentti vaihdellen joko funktion määritelmän muuttujille tai konkreettisille arvoille, jotka välitetään funktiota kutsuttaessa.

Tässä versiossa another_function-funktiosta lisäämme parametrin:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-17-functions-with-parameters/src/main.rs}}
}

Kokeile suorittaa tämä ohjelma; sinun pitäisi saada seuraava tuloste:

{{#include ../listings/ch03-common-programming-concepts/no-listing-17-functions-with-parameters/output.txt}}

another_function-funktion julistuksessa on yksi parametri nimeltä x. x:n tyyppi on määritelty i32:ksi. Kun välitämme 5:n another_function-funktiolle, println!-makro asettaa 5:n muotoilumerkkijonon aaltosulkeisiin, jotka sisältävät x:n.

Funktion allekirjoituksissa sinun täytyy julistaa kunkin parametrin tyyppi. Tämä on tarkoituksellinen päätös Rustin suunnittelussa: Tyyppiannotaatioiden vaatiminen funktioiden määritelmissä tarkoittaa, että kääntäjä tuskin koskaan tarvitsee sinua käyttämään niitä muualla koodissa selvittääkseen, mitä tyyppiä tarkoitat. Kääntäjä pystyy myös antamaan hyödyllisempiä virheilmoituksia, jos se tietää, mitä tyyppejä funktio odottaa.

Kun määrittelet useita parametreja, erota parametrijulistukset pilkuilla, näin:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-18-functions-with-multiple-parameters/src/main.rs}}
}

Tämä esimerkki luo funktion nimeltä print_labeled_measurement kahdella parametrilla. Ensimmäinen parametri on nimeltä value ja on i32. Toinen on nimeltä unit_label ja on tyyppiä char. Funktio tulostaa sitten tekstiä, joka sisältää sekä value- että unit_label-arvot.

Kokeillaan suorittaa tämä koodi. Korvaa functions-projektisi src/main.rs-tiedoston ohjelma edellä olevalla esimerkillä ja suorita se cargo run -komennolla:

{{#include ../listings/ch03-common-programming-concepts/no-listing-18-functions-with-multiple-parameters/output.txt}}

Koska kutsuimme funktiota arvolla 5 value-parametrille ja 'h' unit_label-parametrille, ohjelman tuloste sisältää nämä arvot.

Lausekkeet ja lauseet

Funktioiden rungot koostuvat sarjasta lauseita, jotka valinnaisesti päättyvät lausekkeeseen. Tähän mennessä käsittelemämme funktiot eivät ole sisältäneet päättyvää lauseketta, mutta olet nähnyt lausekkeen osana lausetta. Koska Rust on lausekepohjainen kieli, tämä on tärkeä ero ymmärtää. Muissa kielissä ei ole samoja erotteluja, joten katsotaan, mitä lauseet ja lausekkeet ovat ja miten niiden erot vaikuttavat funktioiden runkoihin.

  • Lauseet ovat ohjeita, jotka suorittavat jonkin toiminnon eivätkä palauta arvoa.
  • Lausekkeet evaluoituvat tulosarvoksi.

Katsotaan joitakin esimerkkejä.

Olemme itse asiassa jo käyttäneet lauseita ja lausekkeita. Muuttujan luominen ja arvon antaminen sille let-avainsanalla on lause. Listauksessa 3-1 let y = 6; on lause.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-01/src/main.rs}}
}
Listing 3-1: main-funktion julistus, joka sisältää yhden lauseen

Funktioiden määritelmät ovat myös lauseita; koko edellinen esimerkki on itsessään lause. (Kuten näemme pian, funktion kutsuminen ei ole lause, vaikka se olisikin.)

Lauseet eivät palauta arvoja. Siksi et voi sijoittaa let-lausetta toiseen muuttujaan, kuten seuraava koodi yrittää; saat virheen:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-19-statements-vs-expressions/src/main.rs}}

Kun suoritat tämän ohjelman, saamasi virhe näyttää tältä:

{{#include ../listings/ch03-common-programming-concepts/no-listing-19-statements-vs-expressions/output.txt}}

let y = 6 -lause ei palauta arvoa, joten x:llä ei ole mitään, mihin sitoutua. Tämä eroaa siitä, mitä tapahtuu muissa kielissä, kuten C:ssä ja Rubyssä, joissa sijoitus palauttaa sijoituksen arvon. Näissä kielissä voit kirjoittaa x = y = 6 ja molemmilla x:llä ja y:llä on arvo 6; näin ei ole Rustissa.

Lausekkeet evaluoituvat arvoksi ja muodostavat suurimman osan muusta koodista, jonka kirjoitat Rustissa. Harkitse matemaattista operaatiota, kuten 5 + 6, joka on lauseke, joka evaluoituu arvoksi 11. Lausekkeet voivat olla osa lauseita: Listauksessa 3-1 6 lauseessa let y = 6; on lauseke, joka evaluoituu arvoksi 6. Funktion kutsuminen on lauseke. Makron kutsuminen on lauseke. Uusi näkyvyysalueen lohko, joka luodaan aaltosulkeilla, on lauseke, esimerkiksi:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-20-blocks-are-expressions/src/main.rs}}
}

Tämä lauseke:

{
    let x = 3;
    x + 1
}

on lohko, joka tässä tapauksessa evaluoituu arvoksi 4. Tämä arvo sidotaan y:hyn osana let-lausetta. Huomaa x + 1 -rivi ilman puolipistettä lopussa, toisin kuin useimmat tähän mennessä näkemäsi rivit. Lausekkeet eivät sisällä päättyvää puolipistettä. Jos lisäät puolipisteen lausekkeen loppuun, muutat sen lauseeksi, eikä se sitten palauta arvoa. Pidä tämä mielessä, kun tutkit funktioiden paluuarvoja ja lausekkeita seuraavaksi.

Funktiot paluuarvoilla

Funktiot voivat palauttaa arvoja koodille, joka kutsuu niitä. Emme nimeä paluuarvoja, mutta meidän täytyy julistaa niiden tyyppi nuolen (->) jälkeen. Rustissa funktion paluuarvo on synonyymi funktion rungon lohkon viimeisen lausekkeen arvolle. Voit palata aikaisin funktiosta return-avainsanalla ja määrittämällä arvon, mutta useimmat funktiot palauttavat viimeisen lausekkeen implisiittisesti. Tässä on esimerkki funktiosta, joka palauttaa arvon:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-21-function-return-values/src/main.rs}}
}

five-funktiossa ei ole funktiokutsuja, makroja eikä edes let-lauseita—vain numero 5 itsestään. Se on täysin kelvollinen funktio Rustissa. Huomaa, että funktion paluutyyppi on myös määritelty -> i32:ksi. Kokeile suorittaa tämä koodi; tulosteen pitäisi näyttää tältä:

{{#include ../listings/ch03-common-programming-concepts/no-listing-21-function-return-values/output.txt}}

5 five-funktiossa on funktion paluuarvo, minkä vuoksi paluutyyppi on i32. Tarkastellaan tätä tarkemmin. Kaksi tärkeää asiaa: Ensinnäkin rivi let x = five(); osoittaa, että käytämme funktion paluuarvoa muuttujan alustamiseen. Koska five-funktio palauttaa 5:n, tämä rivi on sama kuin seuraava:

#![allow(unused)]
fn main() {
let x = 5;
}

Toiseksi five-funktiolla ei ole parametreja ja se määrittelee paluuarvon tyypin, mutta funktion runko on yksinäinen 5 ilman puolipistettä, koska se on lauseke, jonka arvon haluamme palauttaa.

Katsotaan toista esimerkkiä:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-22-function-parameter-and-return/src/main.rs}}
}

Tämän koodin suorittaminen tulostaa The value of x is: 6. Mutta mitä tapahtuu, jos laitamme puolipisteen rivin loppuun, joka sisältää x + 1, muuttaen sen lausekkeesta lauseeksi?

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-23-statements-dont-return-values/src/main.rs}}

Tämän koodin kääntäminen tuottaa virheen seuraavasti:

{{#include ../listings/ch03-common-programming-concepts/no-listing-23-statements-dont-return-values/output.txt}}

Päävirheilmoitus mismatched types paljastaa tämän koodin ydinongelman. plus_one-funktion määritelmä sanoo, että se palauttaa i32:n, mutta lauseet eivät evaluoidu arvoksi, mikä ilmaistaan ():lla, yksikkötyypillä. Siksi mitään ei palauteta, mikä on ristiriidassa funktion määritelmän kanssa ja johtaa virheeseen. Tässä tulosteessa Rust tarjoaa viestin, joka saattaa auttaa korjaamaan ongelman: Se ehdottaa puolipisteen poistamista, mikä korjaisi virheen.

Kommentit

Kommentit

Kaikki ohjelmoijat pyrkivät tekemään koodistaan helposti ymmärrettävää, mutta joskus tarvitaan lisäselitystä. Näissä tapauksissa ohjelmoijat jättävät kommentteja lähdekoodiinsa, joita kääntäjä ohittaa, mutta joita lähdekoodia lukevat ihmiset saattavat pitää hyödyllisinä.

Tässä on yksinkertainen kommentti:

#![allow(unused)]
fn main() {
// hello, world
}

Rustissa idiomaattinen kommenttityyli aloittaa kommentin kahdella kauttaviivalla, ja kommentti jatkuu rivin loppuun. Kommenteille, jotka ulottuvat useammalle riville, sinun täytyy sisällyttää // jokaiselle riville, näin:

#![allow(unused)]
fn main() {
// So we're doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what's going on.
}

Kommentit voidaan myös sijoittaa koodia sisältävien rivien loppuun:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-24-comments-end-of-line/src/main.rs}}
}

Mutta näet ne useammin tässä muodossa, kommentti erillisellä rivillä sen koodin yläpuolella, jota se selittää:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-25-comments-above-line/src/main.rs}}
}

Rustissa on myös toinenlaisia kommentteja, dokumentaatiokommentteja, joita käsittelemme “Craten julkaiseminen Crates.io:hon” -osiossa Luvussa 14.

Ohjausrakenne

Ohjausrakenteet

Mahdollisuus suorittaa koodia riippuen siitä, onko ehto true, ja mahdollisuus suorittaa koodia toistuvasti, kun ehto on true, ovat perusrakennuspalikoita useimmissa ohjelmointikielissä. Yleisimmät rakenteet, jotka antavat hallita Rust-koodin suoritusvirtaa, ovat if-lausekkeet ja silmukat.

if-lausekkeet

if-lauseke antaa haarauttaa koodiasi ehtojen mukaan. Annat ehdon ja sanot sitten: “Jos tämä ehto täyttyy, suorita tämä koodilohko. Jos ehto ei täyty, älä suorita tätä koodilohkoa.”

Luo uusi projekti nimeltä branches projects-kansioosi tutkiaksesi if-lauseketta. Kirjoita src/main.rs-tiedostoon seuraava:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-26-if-true/src/main.rs}}
}

Kaikki if-lausekkeet alkavat avainsanalla if, jota seuraa ehto. Tässä tapauksessa ehto tarkistaa, onko muuttujan number arvo pienempi kuin 5. Sijoitamme koodilohkon, joka suoritetaan, jos ehto on true, heti ehdon jälkeen aaltosulkeiden sisään. if-lausekkeiden ehtoihin liittyviä koodilohkoja kutsutaan joskus haaroiksi, aivan kuten match-lausekkeiden haaroiksi, joita käsittelimme “Arvauksen vertaaminen salaisnumeroon” -osiossa Luvussa 2.

Valinnaisesti voimme myös sisällyttää else-lausekkeen, kuten teimme tässä, antaaksemme ohjelmalle vaihtoehtoisen koodilohkon suoritettavaksi, jos ehto evaluoituu false:ksi. Jos et anna else-lauseketta ja ehto on false, ohjelma ohittaa if-lohkon ja siirtyy seuraavaan koodinpalaan.

Kokeile suorittaa tämä koodi; sinun pitäisi nähdä seuraava tuloste:

{{#include ../listings/ch03-common-programming-concepts/no-listing-26-if-true/output.txt}}

Kokeillaan muuttaa number-muuttujan arvoksi sellainen, joka tekee ehdosta false, ja katsotaan, mitä tapahtuu:

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-27-if-false/src/main.rs:here}}

Suorita ohjelma uudelleen ja katso tuloste:

{{#include ../listings/ch03-common-programming-concepts/no-listing-27-if-false/output.txt}}

On myös syytä huomata, että tämän koodin ehdon täytyy olla bool. Jos ehto ei ole bool, saamme virheen. Kokeile esimerkiksi suorittaa seuraava koodi:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-28-if-condition-must-be-bool/src/main.rs}}

if-ehto evaluoituu tällä kertaa arvoksi 3, ja Rust heittää virheen:

{{#include ../listings/ch03-common-programming-concepts/no-listing-28-if-condition-must-be-bool/output.txt}}

Virhe osoittaa, että Rust odotti bool-tyyppiä mutta sai kokonaisluvun. Toisin kuin kielet kuten Ruby ja JavaScript, Rust ei automaattisesti yritä muuntaa ei-totuusarvoisia tyyppejä totuusarvoiksi. Sinun täytyy olla eksplisiittinen ja antaa if:lle aina totuusarvo ehtona. Jos haluamme if-koodilohkon suorittuvan vain, kun luku ei ole yhtä suuri kuin 0, voimme muuttaa if-lausekkeen seuraavaksi:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-29-if-not-equal-0/src/main.rs}}
}

Tämän koodin suorittaminen tulostaa number was something other than zero.

Useiden ehtojen käsittely else if:llä

Voit käyttää useita ehtoja yhdistämällä if- ja else-lausekkeet else if -lausekkeeseen. Esimerkiksi:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-30-else-if/src/main.rs}}
}

Tällä ohjelmalla on neljä mahdollista polkua. Suoritettuasi sen sinun pitäisi nähdä seuraava tuloste:

{{#include ../listings/ch03-common-programming-concepts/no-listing-30-else-if/output.txt}}

Kun tämä ohjelma suoritetaan, se tarkistaa jokaisen if-lausekkeen vuorollaan ja suorittaa ensimmäisen rungon, jonka ehto evaluoituu true:ksi. Huomaa, että vaikka 6 on jaollinen 2:lla, emme näe tulostetta number is divisible by 2, emmekä number is not divisible by 4, 3, or 2 -tekstiä else-lohkosta. Tämä johtuu siitä, että Rust suorittaa vain ensimmäisen true-ehdon lohkon, eikä tarkista loputkaan sen jälkeen.

Liian monen else if -lausekkeen käyttö voi sotkea koodiasi, joten jos sinulla on useampi kuin yksi, saatat haluta refaktoroida koodisi. Luku 6 kuvaa tehokkaan Rust-haarautumisrakenteen nimeltä match näihin tapauksiin.

if:n käyttö let-lausekkeessa

Koska if on lauseke, voimme käyttää sitä let-lausekkeen oikealla puolella sijoittaaksemme tuloksen muuttujaan, kuten Listauksessa 3-2.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-02/src/main.rs}}
}
Listing 3-2: if-lausekkeen tuloksen sijoittaminen muuttujaan

number-muuttuja sidotaan arvoon if-lausekkeen tuloksen perusteella. Suorita tämä koodi nähdäksesi, mitä tapahtuu:

{{#include ../listings/ch03-common-programming-concepts/listing-03-02/output.txt}}

Muista, että koodilohkot evaluoituvat viimeiseen lausekkeeseensa, ja numerot itsessään ovat myös lausekkeita. Tässä tapauksessa koko if-lausekkeen arvo riippuu siitä, mikä koodilohko suoritetaan. Tämä tarkoittaa, että arvojen, joilla on potentiaalia olla tuloksia kustakin if-haarasta, täytyy olla sama tyyppi; Listauksessa 3-2 sekä if- että else-haaran tulokset olivat i32-kokonaislukuja. Jos tyypit eivät täsmää, kuten seuraavassa esimerkissä, saamme virheen:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-31-arms-must-return-same-type/src/main.rs}}

Kun yritämme kääntää tämän koodin, saamme virheen. if- ja else-haaroilla on yhteensopimattomat arvotyypit, ja Rust osoittaa tarkalleen, mistä ongelma löytyy ohjelmasta:

{{#include ../listings/ch03-common-programming-concepts/no-listing-31-arms-must-return-same-type/output.txt}}

if-lohkon lauseke evaluoituu kokonaisluvuksi, ja else-lohkon lauseke evaluoituu merkkijonoksi. Tämä ei toimi, koska muuttujilla täytyy olla yksi tyyppi, ja Rustin täytyy tietää varmasti käännösaikana, mikä number-muuttujan tyyppi on. number-muuttujan tyypin tunteminen antaa kääntäjälle mahdollisuuden tarkistaa, että tyyppi on kelvollinen kaikkialla, missä käytämme number-muuttujaa. Rust ei pystyisi siihen, jos number-muuttujan tyyppi määräytyisi vasta ajonaikana; kääntäjä olisi monimutkaisempi ja antaisi vähemmän takeita koodista, jos sen täytyisi seurata useita hypoteettisia tyyppejä mille tahansa muuttujalle.

Toisto silmukoilla

On usein hyödyllistä suorittaa koodilohkoa useammin kuin kerran. Tätä tehtävää varten Rust tarjoaa useita silmukoita, jotka suorittavat silmukan rungon koodin loppuun ja alkavat sitten heti alusta. Kokeillaksemme silmukoita luodaan uusi projekti nimeltä loops.

Rustissa on kolmelaista silmukkaa: loop, while ja for. Kokeillaan kutakin.

Koodin toistaminen loop:lla

loop-avainsana käskee Rustia suorittamaan koodilohkon uudelleen ja uudelleen joko ikuisesti tai kunnes kerrot sille eksplisiittisesti lopettamaan.

Esimerkkinä muuta loops-kansiosi src/main.rs-tiedosto näyttämään tältä:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-32-loop/src/main.rs}}

Kun suoritamme tämän ohjelman, näemme again! tulostuvan yhä uudelleen, kunnes pysäytämme ohjelman manuaalisesti. Useimmat terminaalit tukevat näppäinyhdistelmää ctrl-C keskeyttääkseen ohjelman, joka on jumissa jatkuvassa silmukassa. Kokeile:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

Symboli ^C edustaa kohtaa, jossa painoit ctrl-C.

Saatat tai et näe sanaa again! tulostuvan ^C:n jälkeen riippuen siitä, missä koodissa silmukka oli, kun se vastaanotti keskeytystsignaalin.

Onneksi Rust tarjoaa myös tavan poistua silmukasta koodilla. Voit sijoittaa break-avainsanan silmukan sisään kertoaksesi ohjelmalle, milloin lopettaa silmukan suorittaminen. Muistathan, että teimme tämän arvauspelissä “Lopettaminen oikean arvauksen jälkeen” -osiossa Luvussa 2 poistuaksemme ohjelmasta, kun käyttäjä voitti pelin arvaamalla oikean numeron.

Käytimme myös continue-avainsanaa arvauspelissä, joka silmukassa käskee ohjelmaa ohittamaan kaiken jäljellä olevan koodin tässä iteraatiossa ja siirtymään seuraavaan iteraatioon.

Arvojen palauttaminen silmukoista

Yksi loop-silmukan käyttötarkoitus on yrittää uudelleen operaatiota, jonka tiedät saattavan epäonnistua, kuten tarkistaa, onko säie suorittanut työnsä loppuun. Saatat myös tarvita välittää kyseisen operaation tuloksen silmukasta koodisi loppuosan. Voit tehdä tämän lisäämällä palautettavan arvon break-lausekkeen jälkeen, jota käytät silmukan pysäyttämiseen; tämä arvo palautetaan silmukasta, jotta voit käyttää sitä, kuten tässä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-33-return-value-from-loop/src/main.rs}}
}

Ennen silmukkaa julistamme muuttujan nimeltä counter ja alustamme sen arvoon 0. Sitten julistamme muuttujan nimeltä result pitämään silmukasta palautettavan arvon. Jokaisella silmukan iteraatiolla lisäämme 1:n counter-muuttujaan ja tarkistamme sitten, onko counter yhtä suuri kuin 10. Kun se on, käytämme break-avainsanaa arvolla counter * 2. Silmukan jälkeen käytämme puolipistettä lopettaaksemme lausekkeen, joka sijoittaa arvon result:iin. Lopuksi tulostamme result:in arvon, joka tässä tapauksessa on 20.

Voit myös return-palata silmukan sisältä. Vaikka break poistuu vain nykyisestä silmukasta, return poistuu aina nykyisestä funktiosta.

Erottelu silmukkamerkinnöillä

Jos sinulla on silmukoita silmukoiden sisällä, break ja continue koskevat sisintä silmukkaa kyseisessä kohdassa. Voit valinnaisesti määrittää silmukkamerkinnän silmukalle, jota voit sitten käyttää break- tai continue-avainsanojen kanssa määrittääksesi, että nämä avainsanat koskevat merkittyä silmukkaa sisimmän silmukan sijaan. Silmukkamerkintöjen täytyy alkaa yksittäisellä heittomerkillä. Tässä on esimerkki kahdella sisäkkäisellä silmukalla:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-32-5-loop-labels/src/main.rs}}
}

Ulompi silmukka on merkitty 'counting_up, ja se laskee ylöspäin 0:sta 2:een. Sisempi silmukka ilman merkintää laskee alaspäin 10:stä 9:ään. Ensimmäinen break, joka ei määritä merkintää, poistuu vain sisemmästä silmukasta. break 'counting_up; -lauseke poistuu ulommasta silmukasta. Tämä koodi tulostaa:

{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-32-5-loop-labels/output.txt}}

Ehdollisten silmukoiden virtaviivaistaminen while:lla

Ohjelman täytyy usein arvioida ehto silmukan sisällä. Niin kauan kuin ehto on true, silmukka suoritetaan. Kun ehto ei enää ole true, ohjelma kutsuu break:ia pysäyttääkseen silmukan. Tällaisen käyttäytymisen voi toteuttaa yhdistelmällä loop, if, else ja break; voit kokeilla sitä nyt ohjelmassa, jos haluat. Tämä malli on kuitenkin niin yleinen, että Rustissa on siihen sisäänrakennettu kielenrakenne nimeltä while-silmukka. Listauksessa 3-3 käytämme while-silmukkaa silmukan suorittamiseen kolme kertaa, laskemalla alas joka kerta, ja sitten silmukan jälkeen tulostamaan viestin ja poistumaan.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-03/src/main.rs}}
}
Listing 3-3: while-silmukan käyttö koodin suorittamiseen, kun ehto evaluoituu true:ksi

Tämä rakenne poistaa paljon sisäkkäisyyttä, joka olisi tarpeen, jos käyttäisit loop, if, else ja break, ja se on selkeämpi. Niin kauan kuin ehto evaluoituu true:ksi, koodi suoritetaan; muuten silmukasta poistutaan.

Kokoelman läpikäynti for-silmukalla

Voit valita käyttää while-rakennetta silmukan läpikäymiseen kokoelman elementtien yli, kuten taulukon. Esimerkiksi Listauksen 3-4 silmukka tulostaa jokaisen elementin taulukossa a.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-04/src/main.rs}}
}
Listing 3-4: Kokoelman jokaisen elementin läpikäynti while-silmukalla

Tässä koodi laskee ylöspäin taulukon elementtien läpi. Se alkaa indeksistä 0 ja silmukoi, kunnes se saavuttaa taulukon viimeisen indeksin (eli kun index < 5 ei enää ole true). Tämän koodin suorittaminen tulostaa jokaisen elementin taulukossa:

{{#include ../listings/ch03-common-programming-concepts/listing-03-04/output.txt}}

Kaikki viisi taulukon arvoa ilmestyvät terminaaliin odotetusti. Vaikka index saavuttaa arvon 5 jossain vaiheessa, silmukka lopettaa suorittamisen ennen kuin yrittää hakea kuudetta arvoa taulukosta.

Tämä lähestymistapa on virhealtis; voisimme saada ohjelman panikoimaan, jos indeksiarvo tai testiehto on virheellinen. Esimerkiksi jos muuttaisit a-taulukon määritelmää sisältämään neljä elementtiä mutta unohtaisit päivittää ehdon while index < 4:ksi, koodi panikoisi. Se on myös hidas, koska kääntäjä lisää ajonaikaista koodia tarkistamaan jokaisella silmukan iteraatiolla, onko indeksi taulukon rajojen sisällä.

Tiiviimpänä vaihtoehtona voit käyttää for-silmukkaa ja suorittaa koodia jokaiselle kokoelman kohteelle. for-silmukka näyttää Listauksen 3-5 koodilta.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-05/src/main.rs}}
}
Listing 3-5: Kokoelman jokaisen elementin läpikäynti for-silmukalla

Kun suoritamme tämän koodin, näemme saman tulosteen kuin Listauksessa 3-4. Tärkeämpää on, että olemme nyt lisänneet koodin turvallisuutta ja poistaneet bugien mahdollisuuden, jotka voisivat johtua taulukon lopun ylittämisestä tai liian vähäisestä etenemisestä ja joidenkin kohteiden ohittamisesta. Konekoodi, joka generoidaan for-silmukoista, voi myös olla tehokkaampaa, koska indeksiä ei tarvitse verrata taulukon pituuteen jokaisella iteraatiolla.

Käyttämällä for-silmukkaa sinun ei tarvitse muistaa muuttaa muuta koodia, jos muutat taulukon arvojen määrää, toisin kuin Listauksessa 3-4 käytetyllä menetelmällä.

for-silmukoiden turvallisuus ja tiiviys tekevät niistä yleisimmin käytetyn silmukkarakenteen Rustissa. Jopa tilanteissa, joissa haluat suorittaa koodia tietyn määrän kertoja, kuten Listauksen 3-3 while-silmukassa käytetyssä lähtölaskentaesimerkissä, useimmat Rustaceanit käyttäisivät for-silmukkaa. Tapa tehdä se olisi käyttää standardikirjaston tarjoamaa Range-tyyppiä, joka generoi kaikki numerot peräkkäin yhdestä numerosta toiseen numeroon asti.

Tässä on, miltä lähtölaskenta näyttäisi for-silmukalla ja toisella menetelmällä, josta emme ole vielä puhuneet, rev:llä kääntääksemme alueen:

Filename: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-34-for-range/src/main.rs}}
}

Tämä koodi on hieman siistimpi, eikö?

Yhteenveto

Sinä teit sen! Tämä oli laaja luku: Opit muuttujista, skaari- ja yhdistelmätietotyypeistä, funktioista, kommenteista, if-lausekkeista ja silmukoista! Harjoitellaksesi tässä luvussa käsiteltyjä käsitteitä, kokeile rakentaa ohjelmia, jotka:

  • Muuntavat lämpötiloja Fahrenheit- ja Celsius-asteiden välillä.
  • Generoivat n:nnen Fibonacci-luvun.
  • Tulostavat joululaulun “The Twelve Days of Christmas” -laulun sanat hyödyntäen laulun toistoa.

Kun olet valmis jatkamaan, puhumme Rustin käsitteestä, jota ei yleensä ole muissa ohjelmointikielissä: omistajuudesta.

Omistajuuden ymmärtäminen

Omistajuus on Rustin ainutlaatuisin ominaisuus, ja sillä on syvällisiä vaikutuksia koko kieleen. Sen ansiosta Rust pystyy takaamaan muistiturvallisuuden ilman roskienkerääjää, joten on tärkeää ymmärtää, miten omistajuus toimii. Tässä luvussa käsittelemme omistajuutta sekä siihen liittyviä ominaisuuksia: lainaamista, viipaleita ja sitä, miten Rust asettelee tietoa muistiin.

Mikä on omistajuus?

Mikä on omistajuus?

Omistajuus on joukko sääntöjä, jotka määräävät, miten Rust-ohjelma hallitsee muistia. Kaikki ohjelmat joutuvat hallitsemaan muistinkäyttöään suorituksen aikana. Joissakin kielissä on roskienkerääjä, joka säännöllisesti vapauttaa käyttämättömän muistin ohjelman suorittaessa; toisissa kielissä ohjelmoijan on itse varattava ja vapautettava muisti manuaalisesti. Rust käyttää kolmatta lähestymistapaa: muisti hallitaan omistajuusjärjestelmällä, jonka sääntöjä kääntäjä tarkistaa. Jos sääntöjä rikotaan, ohjelma ei käänny. Omistajuuden ominaisuudet eivät hidasta ohjelman suorittamista.

Koska omistajuus on monille ohjelmoijille uusi käsite, sen omaksuminen vie aikaa. Hyvä uutinen on, että mitä enemmän harjoittelet Rustia ja sen omistajuussääntöjä, sitä helpommaksi turvallisen ja tehokkaan koodin kirjoittaminen tulee. Jatka harjoittelua!

Kun ymmärrät omistajuuden, sinulla on vahva pohja Rustin ainutlaatuisten ominaisuuksien ymmärtämiselle. Tässä luvussa opit omistajuudesta käyttämällä esimerkkejä hyvin yleisestä tietorakenteesta: merkkijonoista.

Pino ja keko

Monissa ohjelmointikielissä ei tarvitse ajatella pinon ja keon käyttöä kovin usein. Järjestelmäohjelmointikielessä kuten Rustissa sen sijaan sillä, onko arvo pinossa vai keossa, on vaikutusta kielen käyttäytymiseen ja siihen, miksi tiettyjä päätöksiä täytyy tehdä. Omistajuuden osia kuvataan myöhemmin tässä luvussa pinon ja keon suhteessa, joten tässä on lyhyt selitys valmistautumiseksi.

Sekä pino että keko ovat osia muistista, joita koodisi voi käyttää suorituksen aikana, mutta ne on rakennettu eri tavoin. Pino tallentaa arvot siinä järjestyksessä, jossa ne saapuvat, ja poistaa arvot päinvastaisessa järjestyksessä. Tätä kutsutaan viimeisenä sisään, ensimmäisenä ulos -periaatteeksi. Ajattele lautasten pinoa: kun lisäät lautasia, laitat ne pinon päälle, ja kun tarvitset lautasen, otat yhden pinon päältä. Lautasten lisääminen tai poistaminen pinon keskeltä tai pohjalta ei toimisi yhtä hyvin! Tietojen lisäämistä kutsutaan työntämiseksi pinolle, ja tietojen poistamista poistamiseksi pinolta. Kaikilla pinolla säilytetyillä tiedoilla täytyy olla tunnettu, kiinteä koko. Tiedot, joiden koko ei ole tiedossa käännösaikana tai joiden koko voi muuttua, täytyy sen sijaan tallentaa kekoon.

Keko on vähemmän jäsennelty: kun laitat tietoa kekoon, pyydät tietyn määrän tilaa. Muistin allokoija etsii keosta riittävän tyhjän kohdan, merkitsee sen käytössä olevaksi ja palauttaa osoittimen, joka on kyseisen sijainnin osoite. Tätä prosessia kutsutaan allokoinniksi keossa, ja sitä lyhennetään joskus pelkäksi allokoinniksi (arvojen työntäminen pinolle ei katsota allokoinniksi). Koska osoitin kekoon on tunnetun, kiinteän kokoinen, voit tallentaa osoittimen pinolle, mutta kun tarvitset varsinaiset tiedot, sinun täytyy seurata osoitinta. Ajattele ravintolaa: kun saavut, kerrot ryhmäsi koon, ja isäntä etsii tyhjän pöydän, johon kaikki mahtuvat, ja ohjaa teidät sinne. Jos joku ryhmästäsi saapuu myöhässä, hän voi kysyä, mihin olette istuneet, löytääkseen teidät.

Pinolle työntäminen on nopeampaa kuin allokointi keossa, koska allokoijan ei tarvitse etsiä paikkaa uusille tiedoille; paikka on aina pinon huipulla. Verrattuna siihen allokointi keossa vaatii enemmän työtä, koska allokoijan täytyy ensin löytää riittävän suuri tila tiedoille ja sitten tehdä kirjanpitoa seuraavaa allokointia varten.

Kekoon tallennettujen tietojen käyttö on hitaampaa kuin pinolla olevien tietojen käyttö, koska sinun täytyy seurata osoitinta päästäksesi perille. Nykyaikaiset prosessorit ovat nopeampia, jos ne eivät hyppivät muistissa niin paljon. Jatkamalla vertauskuvaa: ajattele tarjoilijaa, joka ottaa tilauksia useilta pöydiltä. Tehokkainta on ottaa kaikki tilaukset yhdeltä pöydältä ennen siirtymistä seuraavaan. Tilauksen ottaminen pöydältä A, sitten pöydältä B, sitten taas A:sta ja taas B:stä olisi paljon hitaampi prosessi. Samoin prosessori tekee työnsä paremmin, jos se käsittelee tietoja, jotka ovat lähellä toisiaan (kuten pinolla) eivätkä kauempana (kuten keossa voi olla).

Kun koodisi kutsuu funktiota, funktiolle välitetyt arvot (mukaan lukien mahdollisesti osoittimet keon tietoihin) ja funktion paikalliset muuttujat työnnetään pinolle. Kun funktio päättyy, nämä arvot poistetaan pinolta.

Seuranta siitä, mitkä koodin osat käyttävät mitäkin keon tietoja, keon päällekkäisten tietojen määrän minimointi ja käyttämättömien keon tietojen siivoaminen, jotta tila ei lopu, ovat kaikki ongelmia, joita omistajuus ratkaisee. Kun ymmärrät omistajuuden, sinun ei tarvitse ajatella pinon ja keon käyttöä kovin usein, mutta tieto siitä, että omistajuuden pääasiallinen tarkoitus on hallita keon tietoja, auttaa ymmärtämään, miksi se toimii niin kuin se toimii.

Omistajuussäännöt

Ensinnäkin tarkastellaan omistajuuden sääntöjä. Muista nämä säännöt, kun käsittelemme niitä havainnollistavia esimerkkejä:

  • Jokaisella arvolla Rustissa on omistaja.
  • Vain yksi omistaja voi olla kerrallaan.
  • Kun omistaja poistuu näkyvyysalueelta, arvo poistetaan.

Muuttujan näkyvyysalue

Nyt kun olemme käyneet läpi Rustin perussyntaksia, emme sisällytä kaikkiin esimerkkeihin fn main() { -koodia, joten jos seuraat mukana, varmista, että sijoitat seuraavat esimerkit main-funktion sisään manuaalisesti. Näin esimerkkimme ovat hieman tiiviimpiä, jotta voimme keskittyä varsinaisiin yksityiskohtiin eikä pohjakoodiin.

Ensimmäisenä omistajuusesimerkkinä tarkastelemme joidenkin muuttujien näkyvyysaluetta. Näkyvyysalue on ohjelman osa, jossa kohde on voimassa. Tarkastele seuraavaa muuttujaa:

#![allow(unused)]
fn main() {
let s = "hello";
}

Muuttuja s viittaa merkkijonoliteraaliin, jossa merkkijonon arvo on kovakoodattu ohjelman tekstiin. Muuttuja on voimassa siitä hetkestä, jolloin se on määritelty, nykyisen näkyvyysalueen loppuun asti. Listaus 4-1 näyttää ohjelman, jossa kommenteissa on merkitty, missä muuttuja s olisi voimassa.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-01/src/main.rs:here}}
}
Listing 4-1: Muuttuja ja näkyvyysalue, jolla se on voimassa

Toisin sanoen tässä on kaksi tärkeää ajankohtaa:

  • Kun s tulee näkyvyysalueelle, se on voimassa.
  • Se pysyy voimassa, kunnes se poistuu näkyvyysalueelta.

Tässä vaiheessa näkyvyysalueen ja muuttujien voimassaolon välinen suhde on samanlainen kuin muissa ohjelmointikielissä. Rakennamme tämän ymmärryksen päälle esittelemällä String-tyypin.

String-tyyppi

Omistajuussääntöjen havainnollistamiseksi tarvitsemme monimutkaisemman tietotyypin kuin ne, jotka käsittelimme Luvun 3 ”Tietotyypit” -osiossa. Aiemmin käsitellyt tyypit ovat tunnetun kokoisia, ne voidaan tallentaa pinolle ja poistaa pinolta, kun niiden näkyvyysalue päättyy, ja ne voidaan nopeasti ja helposti kopioida uudeksi, riippumattomaksi instanssiksi, jos koodin toinen osa tarvitsee saman arvon eri näkyvyysalueella. Haluamme kuitenkin tarkastella kekoon tallennettuja tietoja ja tutkia, miten Rust tietää, milloin siivota nämä tiedot, ja String-tyyppi on erinomainen esimerkki.

Keskitymme String-tyypin osiin, jotka liittyvät omistajuuteen. Nämä näkökohdat pätevät myös muihin monimutkaisiin tietotyyppeihin, olivatpa ne standardikirjaston tarjoamia tai itse luomiasi. Käsittelemme String-tyyppiä tarkemmin Luvussa 8.

Olemme jo nähneet merkkijonoliteraaleja, joissa merkkijonoarvo on kovakoodattu ohjelmaamme. Merkkijonoliteraalit ovat käteviä, mutta ne eivät sovi kaikkiin tilanteisiin, joissa haluamme käyttää tekstiä. Yksi syy on, että ne ovat muuttumattomia. Toinen on, että kaikkia merkkijonoarvoja ei voida tietää, kun kirjoitamme koodiamme: esimerkiksi jos haluamme ottaa käyttäjän syötteen ja tallentaa sen? Näihin tilanteisiin Rustissa on toinen merkkijonotyyppi, String. Tämä tyyppi hallitsee kekoon allokoituja tietoja ja voi siksi tallentaa tekstimäärän, joka ei ole tiedossa käännösaikana. Voit luoda String-tyypin merkkijonoliteraalista from-funktiolla näin:

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

Kaksoispiste :: -operaattori antaa meidän nimetä tämän tietyn from-funktion String-tyypin alle sen sijaan, että käyttäisimme jotain nimeä kuten string_from. Käsittelemme tätä syntaksia tarkemmin Luvun 5 ”Metodit” -osiossa ja kun puhumme nimiavaruuksista moduuleilla Luvun 7 ”Polut viittaamiseen moduulipuun kohteeseen” -osiossa.

Tämänkaltainen merkkijono voidaan muuttaa:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-01-can-mutate-string/src/main.rs:here}}
}

Eli mikä ero täällä on? Miksi String voidaan muuttaa mutta literaaleja ei? Ero on siinä, miten nämä kaksi tyyppiä käsittelevät muistia.

Muisti ja allokointi

Merkkijonoliteraalin tapauksessa tiedämme sisällön käännösaikana, joten teksti kovakoodataan suoraan lopulliseen suoritettavaan tiedostoon. Siksi merkkijonoliteraalit ovat nopeita ja tehokkaita. Nämä ominaisuudet johtuvat kuitenkin merkkijonoliteraalin muuttumattomuudesta. Valitettavasti emme voi laittaa muistipätkää binääritiedostoon jokaiselle tekstiosalle, jonka koko ei ole tiedossa käännösaikana ja jonka koko voi muuttua ohjelman suorituksen aikana.

String-tyypin tapauksessa muuttuvan, kasvavan tekstin tukemiseksi meidän täytyy allokoida kekoon tietty määrä muistia, joka ei ole tiedossa käännösaikana, sisällön säilyttämiseksi. Tämä tarkoittaa:

  • Muisti täytyy pyytää muistin allokoijalta suorituksen aikana.
  • Tarvitsemme tavan palauttaa tämä muisti allokoijalle, kun olemme valmiita String-tyypin kanssa.

Ensimmäisen osan teemme itse: kun kutsumme String::from, sen toteutus pyytää tarvitsemansa muistin. Tämä on melkein yleispätevää kaikissa ohjelmointikielissä.

Toinen osa on kuitenkin erilainen. Kielissä, joissa on roskienkerääjä (GC), GC seuraa ja siivoaa muistin, jota ei enää käytetä, emmekä meidän tarvitse ajatella sitä. Useimmissa kielissä ilman GC:tä vastuullamme on tunnistaa, milloin muistia ei enää käytetä, ja kutsua koodia vapauttamaan se eksplisiittisesti, aivan kuten teimme pyytäessämme sitä. Tämän tekeminen oikein on historiallisesti ollut vaikea ohjelmointiongelma. Jos unohdamme, tuhlaamme muistia. Jos teemme sen liian aikaisin, meillä on virheellinen muuttuja. Jos teemme sen kahdesti, sekin on bugi. Meidän täytyy yhdistää täsmälleen yksi allocate täsmälleen yhteen free-kutsuun.

Rust valitsee eri polun: muisti palautetaan automaattisesti, kun muuttujan, joka omistaa sen, näkyvyysalue päättyy. Tässä on versio näkyvyysalueesimerkistämme Listauksesta 4-1 käyttäen String-tyyppiä merkkijonoliteraalin sijaan:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-02-string-scope/src/main.rs:here}}
}

On luonnollinen kohta, jossa voimme palauttaa String-tyypin tarvitseman muistin allokoijalle: kun s poistuu näkyvyysalueelta. Kun muuttuja poistuu näkyvyysalueelta, Rust kutsuu erityistä funktiota puolestamme. Tätä funktiota kutsutaan drop -funktioksi, ja siihen String-tyypin tekijä voi sijoittaa koodin muistin palauttamiseksi. Rust kutsuu drop-funktiota automaattisesti sulkevassa aaltosulussa.

Huom: C++:ssa tätä resurssien vapauttamisen mallia kohteen elinkaaren lopussa kutsutaan joskus Resource Acquisition Is Initialization (RAII) -malliksi. Rustin drop-funktio on tuttu sinulle, jos olet käyttänyt RAII-malleja.

Tällä mallilla on syvällinen vaikutus siihen, miten Rust-koodia kirjoitetaan. Se saattaa vaikuttaa yksinkertaiselta nyt, mutta koodin käyttäytyminen voi olla odottamatonta monimutkaisemmissa tilanteissa, kun haluamme useiden muuttujien käyttävän kekoon allokoimiamme tietoja. Tutkitaan joitakin näistä tilanteista nyt.

Muuttujien ja tietojen vuorovaikutus siirron (move) kautta

Useat muuttujat voivat vuorovaikuttaa samojen tietojen kanssa eri tavoin Rustissa. Tarkastellaan esimerkkiä kokonaisluvulla Listauksessa 4-2.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-02/src/main.rs:here}}
}
Listing 4-2: Kokonaisluvun arvon x sitominen muuttujaan y

Voimme todennäköisesti arvata, mitä tämä tekee: ”sido arvo 5 muuttujaan x; tee sitten kopio arvosta x:ssä ja sido se muuttujaan y.” Meillä on nyt kaksi muuttujaa, x ja y, ja molemmat ovat 5. Näin todellakin tapahtuu, koska kokonaisluvut ovat yksinkertaisia arvoja tunnetulla, kiinteällä koolla, ja nämä kaksi 5-arvoa työnnetään pinolle.

Tarkastellaan nyt String-versiota:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-03-string-move/src/main.rs:here}}
}

Tämä näyttää hyvin samankaltaiselta, joten voisimme olettaa, että se toimisi samalla tavalla: toinen rivi tekisi kopion arvosta s1:ssä ja sitoisi sen s2:een. Mutta näin ei aivan tapahdu.

Katso Kuvaa 4-1 nähdäksesi, mitä String-tyypille tapahtuu konepellin alla. String koostuu kolmesta osasta, jotka näkyvät vasemmalla: osoitin muistiin, joka sisältää merkkijonon sisällön, pituus ja kapasiteetti. Tämä tietoryhmä tallennetaan pinolle. Oikealla on keon muisti, joka sisältää sisällön.

Two tables: the first table contains the representation of s1 on the
stack, consisting of its length (5), capacity (5), and a pointer to the first
value in the second table. The second table contains the representation of the
string data on the heap, byte by byte.

Kuva 4-1: String-tyypin esitys muistissa, kun se sisältää arvon "hello" ja on sidottu s1:een

Pituus on se, kuinka paljon muistia tavussa String-tyypin sisältö tällä hetkellä käyttää. Kapasiteetti on kokonaismäärä muistia tavuina, jonka String on saanut allokoijalta. Ero pituuden ja kapasiteetin välillä on merkityksellinen, mutta ei tässä kontekstissa, joten toistaiseksi kapasiteetin voi jättää huomiotta.

Kun sidomme s1:n s2:een, String-tyypin tiedot kopioidaan, eli kopioimme pinolla olevan osoittimen, pituuden ja kapasiteetin. Emme kopioi keon tietoja, joihin osoitin viittaa. Toisin sanoen tietojen esitys muistissa näyttää Kuvalta 4-2.

Three tables: tables s1 and s2 representing those strings on the
stack, respectively, and both pointing to the same string data on the heap.

Kuva 4-2: Muuttujan s2 esitys muistissa, jolla on kopio s1:n osoittimesta, pituudesta ja kapasiteetista

Esitys ei näytä Kuvalta 4-3, miltä muisti näyttäisi, jos Rust kopioisi myös keon tiedot. Jos Rust tekisi näin, operaatio s2 = s1 voisi olla erittäin kallis suorituskyvyn kannalta, jos keon tiedot olisivat suuria.

Four tables: two tables representing the stack data for s1 and s2,
and each points to its own copy of string data on the heap.

Kuva 4-3: Toinen mahdollisuus sille, mitä s2 = s1 voisi tehdä, jos Rust kopioisi myös keon tiedot

Aiemmin sanoin, että kun muuttuja poistuu näkyvyysalueelta, Rust kutsuu automaattisesti drop-funktion ja siivoaa kyseisen muuttujan keon muistin. Kuva 4-2 kuitenkin näyttää molempien tietojen osoittimien osoittavan samaan sijaintiin. Tämä on ongelma: kun s2 ja s1 poistuvat näkyvyysalueelta, ne yrittävät molemmat vapauttaa saman muistin. Tätä kutsutaan kaksoisvapautus-virheeksi, ja se on yksi aiemmin mainituista muistiturvallisuusbugeista. Muistin vapauttaminen kahdesti voi johtaa muistin korruptoitumiseen, mikä voi puolestaan johtaa tietoturva-aukkoja.

Muistiturvallisuuden varmistamiseksi rivin let s2 = s1; jälkeen Rust pitää s1:tä enää voimassa olemattomana. Siksi Rustin ei tarvitse vapauttaa mitään, kun s1 poistuu näkyvyysalueelta. Katso, mitä tapahtuu, kun yrität käyttää s1:tä s2:n luomisen jälkeen; se ei toimi:

{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-04-cant-use-after-move/src/main.rs:here}}

Saat virheen, joka on tämänkaltainen, koska Rust estää sinua käyttämästä mitätöityä viitettä:

{{#include ../listings/ch04-understanding-ownership/no-listing-04-cant-use-after-move/output.txt}}

Jos olet kuullut termit pinnallinen kopiointi ja syvä kopiointi työskennellessäsi muiden kielten kanssa, käsite osoittimen, pituuden ja kapasiteetin kopioimisesta kopioimatta tietoja kuulostaa todennäköisesti pinnalliselta kopioinnilta. Koska Rust myös mitätöi ensimmäisen muuttujan, sitä ei kutsuta pinnalliseksi kopioinniksi vaan siirroksi (move). Tässä esimerkissä sanoisimme, että s1 siirrettiin s2:een. Eli mitä todella tapahtuu, näkyy Kuvassa 4-4.

Three tables: tables s1 and s2 representing those strings on the
stack, respectively, and both pointing to the same string data on the heap.
Table s1 is grayed out be-cause s1 is no longer valid; only s2 can be used to
access the heap data.

Kuva 4-4: Esitys muistissa sen jälkeen, kun s1 on mitätöity

Tämä ratkaisee ongelmamme! Kun vain s2 on voimassa, se yksin vapauttaa muistin poistuessaan näkyvyysalueelta, ja olemme valmiita.

Lisäksi tässä on implisiittinen suunnittelupäätös: Rust ei koskaan luo automaattisesti tietojesi ”syviä” kopioita. Siksi voidaan olettaa, että mikä tahansa automaattinen kopiointi on suorituskyvyn kannalta edullista.

Näkyvyysalue ja sijoitus

Tämän käänteinen on totta myös näkyvyysalueen, omistajuuden ja drop-funktion kautta vapautetun muistin suhteen. Kun sijoitat täysin uuden arvon olemassa olevaan muuttujaan, Rust kutsuu drop-funktion ja vapauttaa alkuperäisen arvon muistin välittömästi. Tarkastele esimerkiksi tätä koodia:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-04b-replacement-drop/src/main.rs:here}}
}

Määrittelemme aluksi muuttujan s ja sidomme sen String-tyyppiin arvolla "hello". Sitten luomme heti uuden String-tyypin arvolla "ahoy" ja sijoitamme sen s:ään. Tässä vaiheessa mikään ei enää viittaa alkuperäiseen keon arvoon.

One table s representing the string value on the stack, pointing to
the second piece of string data (ahoy) on the heap, with the original string
data (hello) grayed out because it cannot be accessed anymore.

Kuva 4-5: Esitys muistissa sen jälkeen, kun alkuperäinen arvo on korvattu kokonaan.

Alkuperäinen merkkijono poistuu siis välittömästi näkyvyysalueelta. Rust suorittaa drop-funktion sille, ja sen muisti vapautetaan heti. Kun tulostamme arvon lopussa, se on "ahoy, world!".

Muuttujien ja tietojen vuorovaikutus kloonauksen (clone) kautta

Jos haluamme kopioida String-tyypin keon tiedot syvästi, eikä vain pinon tietoja, voimme käyttää yleistä clone-metodia. Käsittelemme metodisyntaksia Luvussa 5, mutta koska metodit ovat yleisiä monissa ohjelmointikielissä, olet todennäköisesti nähnyt ne aiemmin.

Tässä on esimerkki clone-metodista käytössä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-05-clone/src/main.rs:here}}
}

Tämä toimii hyvin ja tuottaa eksplisiittisesti Kuvassa 4-3 näytetyn käyttäytymisen, jossa keon tiedot todella kopioidaan.

Kun näet clone-kutsun, tiedät, että jotain mielivaltaista koodia suoritetaan ja se koodi voi olla kallista. Se on visuaalinen merkki siitä, että jotain erilaista on meneillään.

Vain pinolla olevat tiedot: Copy

Meillä on vielä yksi mutka, josta emme ole puhuneet. Tämä koodi kokonaisluvuilla — osa näytettiin Listauksessa 4-2 — toimii ja on kelvollinen:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-06-copy/src/main.rs:here}}
}

Mutta tämä koodi näyttää ristiriitaiselta sen suhteen, mitä juuri opimme: meillä ei ole clone-kutsua, mutta x on silti voimassa eikä sitä siirretty y:hyn.

Syy on, että tyypit kuten kokonaisluvut, joiden koko on tiedossa käännösaikana, tallennetaan kokonaan pinolle, joten varsinaisten arvojen kopiot ovat nopeita tehdä. Tämä tarkoittaa, ettei ole syytä estää x:ää olemassa muuttujan y luomisen jälkeen. Toisin sanoen syvän ja pinnallisen kopioinnin välillä ei ole eroa tässä, joten clone-kutsu ei tekisi mitään erilaista tavalliseen pinnalliseen kopiointiin verrattuna, ja voimme jättää sen pois.

Rustissa on erityinen annotaatio nimeltä Copy-trait, jonka voimme sijoittaa pinolla tallennettuihin tyyppeihin, kuten kokonaisluvut (puhumme traiteista tarkemmin Luvussa 10). Jos tyyppi toteuttaa Copy-traitin, sitä käyttävät muuttujat eivät siirry vaan kopioidaan triviaalisti, joten ne ovat edelleen voimassa toiseen muuttujaan sijoittamisen jälkeen.

Rust ei anna meidän annotoida tyyppiä Copy:lla, jos tyyppi tai jokin sen osista on toteuttanut Drop-traitin. Jos tyypille täytyy tapahtua jotain erityistä, kun arvo poistuu näkyvyysalueelta, ja lisäämme Copy-annotaation kyseiseen tyyppiin, saamme käännösaikaisen virheen. Opit lisäämään Copy-annotaation omaan tyyppiisi traitin toteuttamiseksi liitteessä C kohdassa ”Johdettavat traitit”.

Mitä tyypit toteuttavat Copy-traitin? Voit tarkistaa tietyn tyypin dokumentaatiosta varmuuden vuoksi, mutta yleissääntönä mikä tahansa yksinkertaisten skalaariarvojen ryhmä voi toteuttaa Copy:n, eikä mikään, mikä vaatii allokointia tai on jonkinlainen resurssi, voi toteuttaa Copy:tä. Tässä on joitakin Copy:n toteuttavia tyyppejä:

  • Kaikki kokonaislukutyypit, kuten u32.
  • Totuusarvotyyppi bool arvoilla true ja false.
  • Kaikki liukulukutyypit, kuten f64.
  • Merkkityyppi char.
  • Tuplet, jos ne sisältävät vain tyyppejä, jotka myös toteuttavat Copy:n. Esimerkiksi (i32, i32) toteuttaa Copy:n, mutta (i32, String) ei.

Omistajuus ja funktiot

Arvon välittämisen mekaniikka funktiolle on samanlainen kuin arvon sijoittaminen muuttujaan. Muuttujan välittäminen funktiolle siirtää tai kopioi sen, aivan kuten sijoitus. Listaus 4-3 sisältää esimerkin, jossa on annotaatioita siitä, missä muuttujat tulevat näkyvyysalueelle ja poistuvat sieltä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-03/src/main.rs}}
}
Listing 4-3: Funktiot omistajuus- ja näkyvyysaluekommentein

Jos yrittäisimme käyttää s:tä takes_ownership-kutsun jälkeen, Rust antaisi käännösaikaisen virheen. Nämä staattiset tarkistukset suojaavat meitä virheiltä. Kokeile lisätä main-funktioon koodia, joka käyttää s:tä ja x:ää, nähdäksesi missä voit käyttää niitä ja missä omistajuussäännöt estävät sen.

Paluuarvot ja näkyvyysalue

Paluuarvot voivat myös siirtää omistajuutta. Listaus 4-4 näyttää esimerkin funktiosta, joka palauttaa arvon, samankaltaisilla annotaatioilla kuin Listauksessa 4-3.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-04/src/main.rs}}
}
Listing 4-4: Paluuarvojen omistajuuden siirtäminen

Muuttujan omistajuus noudattaa samaa kaavaa joka kerta: arvon sijoittaminen toiseen muuttujaan siirtää sen. Kun muuttuja, joka sisältää keon tietoja, poistuu näkyvyysalueelta, drop siivoaa arvon, ellei tietojen omistajuutta ole siirretty toiseen muuttujaan.

Vaikka tämä toimii, omistajuuden ottaminen ja palauttaminen jokaisella funktiolla on hieman työlästä. Entä jos haluamme antaa funktion käyttää arvoa ottamatta omistajuutta? On ärsyttävää, että kaiken mitä välitämme sisään täytyy myös välittää takaisin, jos haluamme käyttää sitä uudelleen, lisäksi mihin tahansa funktion rungosta tulevaan dataan, jonka haluamme ehkä palauttaa.

Rust antaa meidän palauttaa useita arvoja tuplen avulla, kuten Listauksessa 4-5 näytetään.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-05/src/main.rs}}
}
Listing 4-5: Parametrien omistajuuden palauttaminen

Mutta tämä on liikaa seremoniaa ja työtä käsitteelle, jonka pitäisi olla yleinen. Onneksi Rustissa on ominaisuus arvon käyttämiseen siirtämättä omistajuutta, nimeltä viitteet.

Viitteet ja lainaus

Viittaukset ja lainaaminen

Listauksen 4-5 tuplakoodin ongelma on, että meidän täytyy palauttaa String kutsuvalle funktiolle, jotta voimme edelleen käyttää String-arvoa calculate_length-kutsun jälkeen, koska String siirtyi calculate_length-funktion omistukseen. Sen sijaan voimme tarjota viittauksen String-arvoon. Viittaus on kuin osoitin siinä mielessä, että se on osoite, jota seuraamalla pääsemme kyseiseen osoitteeseen tallennettuun dataan; tämän datan omistaa jokin toinen muuttuja. Toisin kuin osoitin, viittaus on taattu osoittamaan kelvollista tietyn tyyppistä arvoa koko viittauksen eliniän ajan.

Näin määrittelet ja käytät calculate_length-funktiota, joka ottaa viittauksen objektiin parametrinaan omistajuuden siirtämisen sijaan:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-07-reference/src/main.rs:all}}
}

Ensinnäkin huomaa, että kaikki tuplakoodi muuttujan määrityksessä ja funktion paluuarvossa on poissa. Toiseksi huomaa, että välitämme &s1:n calculate_length-funktiolle ja sen määrityksessä otamme &String-tyypin String-tyypin sijaan. Nämä et-merkit edustavat viittauksia, ja niiden avulla voit viitata johonkin arvoon ottamatta sen omistajuutta. Kuva 4-6 havainnollistaa tätä käsitettä.

Three tables: the table for s contains only a pointer to the table
for s1. The table for s1 contains the stack data for s1 and points to the
string data on the heap.

Kuva 4-6: Kaavio &String-tyypin s:stä, joka osoittaa String-tyypin s1:een

Huom: Viittaamisen vastakohta &-merkillä on dereferointi, joka tehdään dereferointioperaattorilla *. Näemme dereferointioperaattorin käyttöä Luvussa 8 ja käsittelemme dereferoinnin yksityiskohdat Luvussa 15.

Katsotaan funktiokutsua tarkemmin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-07-reference/src/main.rs:here}}
}

&s1-syntaksi antaa meidän luoda viittauksen, joka viittaa s1:n arvoon mutta ei omista sitä. Koska viittaus ei omista sitä, arvoa, johon se osoittaa, ei vapauteta, kun viittaus lakkaa olemasta käytössä.

Samoin funktion signatuuri käyttää &-merkkiä ilmaisemaan, että parametrin s tyyppi on viittaus. Lisätään selittäviä annotaatioita:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-08-reference-with-annotations/src/main.rs:here}}
}

Näkyvyysalue, jolla muuttuja s on kelvollinen, on sama kuin minkä tahansa funktion parametrin näkyvyysalue, mutta viittauksen osoittamaa arvoa ei vapauteta, kun s lakkaa olemasta käytössä, koska s:llä ei ole omistajuutta. Kun funktioilla on viittauksia parametreina todellisten arvojen sijaan, meidän ei tarvitse palauttaa arvoja omistajuuden palauttamiseksi, koska emme koskaan omistaneet niitä.

Kutsumme viittauksen luomista lainaamiseksi. Kuten oikeassa elämässä, jos henkilö omistaa jotain, voit lainata sen häneltä. Kun olet valmis, sinun täytyy palauttaa se. Et omista sitä.

Mitä tapahtuu, jos yritämme muuttaa jotain, mitä lainaamme? Kokeile Listauksen 4-6 koodia. Varoitus: se ei toimi!

Filename: src/main.rs
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-06/src/main.rs}}
Listing 4-6: Yritys muuttaa lainattua arvoa

Tässä on virhe:

{{#include ../listings/ch04-understanding-ownership/listing-04-06/output.txt}}

Aivan kuten muuttujat ovat oletusarvoisesti muuttumattomia, viittauksetkin ovat. Emme saa muuttaa jotain, johon meillä on viittaus.

Muuttuvat viittaukset

Voimme korjata Listauksen 4-6 koodin sallimaan lainatun arvon muuttamisen muutamalla pienellä muutoksella, jotka käyttävät muuttuvaa viittausta:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-09-fixes-listing-04-06/src/main.rs}}
}

Ensin muutamme s:n muotoon mut. Sitten luomme muuttuvan viittauksen &mut s:llä, kun kutsumme change-funktiota, ja päivitämme funktion signatuurin hyväksymään muuttuvan viittauksen some_string: &mut String. Tämä tekee hyvin selväksi, että change-funktio muuttaa lainaamaansa arvoa.

Muuttuvilla viittauksilla on yksi suuri rajoitus: jos sinulla on muuttuva viittaus arvoon, sinulla ei voi olla muita viittauksia kyseiseen arvoon. Tämä koodi, joka yrittää luoda kaksi muuttuvaa viittausta s:ään, epäonnistuu:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-10-multiple-mut-not-allowed/src/main.rs:here}}

Tässä on virhe:

{{#include ../listings/ch04-understanding-ownership/no-listing-10-multiple-mut-not-allowed/output.txt}}

Tämä virheilmoitus sanoo, että koodi on virheellinen, koska emme voi lainata s:ää muuttuvana useammin kuin kerran kerrallaan. Ensimmäinen muuttuva lainaus on r1:ssä ja sen täytyy kestää siihen asti, kunnes sitä käytetään println!:ssä, mutta tuon muuttuvan viittauksen luomisen ja sen käytön välissä yritimme luoda toisen muuttuvan viittauksen r2:ssa, joka lainaa samoja tietoja kuin r1.

Rajoitus, joka estää useita muuttuvia viittauksia samoihin tietoihin samaan aikaan, sallii mutaation, mutta hyvin kontrolloidulla tavalla. Uusien rustilaisien on vaikea tottua siihen, koska useimmat kielet sallivat mutaation milloin tahansa. Tämän rajoituksen hyöty on, että Rust voi estää data race -tilanteet käännösaikana. Data race on samankaltainen kuin kilpailutilanne (race condition), ja se tapahtuu, kun nämä kolme käyttäytymistä esiintyvät:

  • Kaksi tai useampi osoitin käyttää samoja tietoja samaan aikaan.
  • Vähintään yhtä osoittimista käytetään kirjoittamaan dataan.
  • Mitään mekanismia ei käytetä synkronoimaan pääsyä dataan.

Data race -tilanteet aiheuttavat määrittelemätöntä käyttäytymistä, ja niitä voi olla vaikea diagnosoida ja korjata ajonaikana; Rust estää tämän ongelman kieltäytymällä kääntämästä data race -tilanteita sisältävää koodia!

Kuten aina, voimme käyttää aaltosulkeita luodaksemme uuden näkyvyysalueen, mikä sallii useita muuttuvia viittauksia, mutta ei samanaikaisia:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-11-muts-in-separate-scopes/src/main.rs:here}}
}

Rust pakottaa samanlaisen säännön muuttuvien ja muuttumattomien viittausten yhdistämiselle. Tämä koodi johtaa virheeseen:

{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-12-immutable-and-mutable-not-allowed/src/main.rs:here}}

Tässä on virhe:

{{#include ../listings/ch04-understanding-ownership/no-listing-12-immutable-and-mutable-not-allowed/output.txt}}

Huh! Emme myöskään voi omistaa muuttuvaa viittausta, kun meillä on muuttumaton viittaus samaan arvoon.

Muuttumattoman viittauksen käyttäjät eivät odota arvon muuttuvan yllättäen altaan! Useita muuttumattomia viittauksia sallitaan kuitenkin, koska kukaan, joka vain lukee dataa, ei voi vaikuttaa kenenkään muun datan lukemiseen.

Huomaa, että viittauksen näkyvyysalue alkaa siitä, missä se esitellään, ja jatkuu viimeiseen kertaan, jolloin viittausta käytetään. Esimerkiksi tämä koodi kääntyy, koska muuttumattomien viittausten viimeinen käyttö on println!:ssä, ennen kuin muuttuva viittaus esitellään:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-13-reference-scope-ends/src/main.rs:here}}
}

Muuttumattomien viittausten r1:n ja r2:n näkyvyysalueet päättyvät println!:n jälkeen, missä niitä käytetään viimeisen kerran, eli ennen kuin muuttuva viittaus r3 luodaan. Nämä näkyvyysalueet eivät limitty, joten tämä koodi on sallittu: kääntäjä voi päätellä, ettei viittausta enää käytetä ennen näkyvyysalueen päättymistä.

Vaikka lainausvirheet voivat joskus turhauttaa, muista, että Rust-kääntäjä osoittaa mahdollisen bugin varhain (käännösaikana ajonaikaisen sijaan) ja näyttää tarkalleen, missä ongelma on. Sinun ei sitten tarvitse selvittää, miksi datasi ei ole sitä, mitä luulit sen olevan.

Riippuvat viittaukset

Kielissä, joissa on osoittimia, on helppo luoda virheellisesti riippuvan osoittimen — osoittimen, joka viittaa muistipaikkaan, joka on saatettu antaa jollekulle muulle — vapauttamalla muistia mutta säilyttämällä osoitin kyseiseen muistiin. Rustissa kääntäjä puolestaan takaa, ettei viittauksia koskaan ole riippuvia: jos sinulla on viittaus johonkin dataan, kääntäjä varmistaa, ettei data poistu näkyvyysalueelta ennen kuin viittaus siihen poistuu.

Yritetään luoda riippuva viittaus nähdäksemme, miten Rust estää ne käännösaikaisella virheellä:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-14-dangling-reference/src/main.rs}}

Tässä on virhe:

{{#include ../listings/ch04-understanding-ownership/no-listing-14-dangling-reference/output.txt}}

Tämä virheilmoitus viittaa ominaisuuteen, jota emme ole vielä käsitelleet: elinikäihin. Käsittelemme elinikäitä yksityiskohtaisesti Luvussa 10. Mutta jos sivuutat elinikäisiin liittyvät osat, viesti sisältää avaimen siihen, miksi tämä koodi on ongelma:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

Katsotaan tarkemmin, mitä tapahtuu dangle-koodimme jokaisessa vaiheessa:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-15-dangling-reference-annotated/src/main.rs:here}}

Koska s luodaan dangle-funktion sisällä, kun dangle-funktion koodi on valmis, s vapautetaan. Mutta yritimme palauttaa viittauksen siihen. Tämä tarkoittaa, että viittaus osoittaisi virheelliseen String-arvoon. Se ei käy! Rust ei anna meidän tehdä tätä.

Ratkaisu on palauttaa String suoraan:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-16-no-dangle/src/main.rs:here}}
}

Tämä toimii ilman ongelmia. Omistajuus siirtyy ulos, eikä mitään vapauteta.

Viittausten säännöt

Kerrataan, mitä olemme käsitelleet viittauksista:

  • Milloin tahansa voit omistaa joko yhden muuttuvan viittauksen tai minkä tahansa määrän muuttumattomia viittauksia.
  • Viittausten on aina oltava kelvollisia.

Seuraavaksi tarkastelemme erilaista viittaustyyppiä: sliceja.

Viipale-tyyppi

Slice-tyyppi

Slicet antavat sinun viitata kokoelman peräkkäiseen elementtijaksoon koko kokoelman sijaan. Slice on eräänlainen viite, joten sillä ei ole omistajuutta.

Tässä on pieni ohjelmointiongelma: kirjoita funktio, joka ottaa välilyönneillä erotetun merkkijonon sanoja ja palauttaa ensimmäisen sanan, jonka se löytää kyseisestä merkkijonosta. Jos funktio ei löydä välilyöntiä merkkijonosta, koko merkkijonon täytyy olla yksi sana, joten koko merkkijono pitäisi palauttaa.

Käydään läpi, miten kirjoittaisimme tämän funktion signatuurin käyttämättä sliceja, jotta ymmärrämme ongelman, jonka slicet ratkaisevat:

fn first_word(s: &String) -> ?

first_word-funktiolla on parametrina &String. Emme tarvitse omistajuutta, joten tämä on hyvä. (Idiomatiisessa Rustissa funktiot eivät ota omistajuutta argumenteistaan, elleivät sitä tarvitse, ja syyt siihen selviävät selväksi jatkaessamme!) Mutta mitä meidän pitäisi palauttaa? Meillä ei oikeastaan ole tapaa puhua merkkijonon osasta. Voisimme kuitenkin palauttaa sanan lopun indeksin, jonka ilmaisee välilyönti. Kokeillaan sitä, kuten Listauksessa 4-7.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:here}}
}
Listing 4-7: first_word-funktio, joka palauttaa tavuintiarvon String-parametrin sisällä

Koska meidän täytyy käydä String läpi elementti kerrallaan ja tarkistaa, onko arvo välilyönti, muunnamme String-mme tavutaulukoksi käyttämällä as_bytes-metodia.

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:as_bytes}}

Seuraavaksi luomme iteraattorin tavutaulukon yli käyttämällä iter-metodia:

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:iter}}

Käsittelemme iteraattoreita yksityiskohtaisemmin Luvussa 13. Toistaiseksi tiedä, että iter on metodi, joka palauttaa jokaisen elementin kokoelmassa ja että enumerate käärii iter-metodin tuloksen ja palauttaa jokaisen elementin osana monikkoa sen sijaan. Monikon ensimmäinen elementti, jonka enumerate palauttaa, on indeksi, ja toinen elementti on viite elementtiin. Tämä on hieman kätevämpää kuin indeksin laskeminen itse.

Koska enumerate-metodi palauttaa monikon, voimme käyttää kuvioita purkamaan tuon monikon. Käsittelemme kuvioita lisää Luvussa 6. for-silmukassa määritämme kuvion, jossa on i monikon indeksille ja &item monikon yksittäiselle tavulle. Koska saamme viitteen elementtiin .iter().enumerate()-ketjusta, käytämme kuviossa &-merkkiä.

for-silmukan sisällä etsimme välilyöntiä edustavaa tavua käyttämällä tavuliteraalisyntaksia. Jos löydämme välilyönnin, palautamme sijainnin. Muuten palautamme merkkijonon pituuden käyttämällä s.len().

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:inside_for}}

Meillä on nyt tapa selvittää ensimmäisen sanan lopun indeksi merkkijonossa, mutta on ongelma. Palautamme usize-arvon yksinään, mutta se on merkityksellinen luku vain &String-kontekstissa. Toisin sanoen, koska se on erillinen arvo String-arvosta, ei ole mitään takeita siitä, että se olisi edelleen kelvollinen tulevaisuudessa. Harkitse Listauksen 4-8 ohjelmaa, joka käyttää Listauksen 4-7 first_word-funktiota.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-08/src/main.rs:here}}
}
Listing 4-8: first_word-funktion kutsumisen tuloksen tallentaminen ja String-sisällön muuttaminen

Tämä ohjelma kääntyy ilman virheitä ja tekisi niin myös, jos käyttäisimme word-muuttujaa s.clear()-kutsun jälkeen. Koska word ei ole lainkaan yhteydessä s-muuttujan tilaan, word sisältää edelleen arvon 5. Voisimme käyttää arvoa 5 muuttujan s kanssa yrittääksemme poimia ensimmäisen sanan, mutta tämä olisi bugi, koska s-muuttujan sisältö on muuttunut siitä, kun tallensimme arvon 5 muuttujaan word.

Huolehtiminen siitä, että word-muuttujan indeksi menee epäsynkkaan s-muuttujan datan kanssa, on työlästä ja virhealtista! Näiden indeksien hallinta on vieläkin hauraampaa, jos kirjoitamme second_word-funktion. Sen signatuurin pitäisi näyttää tältä:

fn second_word(s: &String) -> (usize, usize) {

Nyt seuraamme sekä aloitus- että lopetusindeksiä, ja meillä on vielä enemmän arvoja, jotka laskettiin tietyn tilan datasta mutta eivät ole lainkaan sidottuja siihen tilaan. Meillä on kolme toisiinsa liittymätöntä muuttujaa, jotka täytyy pitää synkassa.

Onneksi Rustilla on ratkaisu tähän ongelmaan: merkkijonoslicet.

Merkkijonoslicet

Merkkijonoslice on viite String-arvon osaan, ja se näyttää tältä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-17-slice/src/main.rs:here}}
}

Sen sijaan, että viittaisimme koko String-arvoon, hello on viite String-arvon osaan, joka määritellään ylimääräisessä [0..5]-osassa. Luomme slicet käyttämällä hakasulkeissa olevaa väliä määrittämällä [aloitusindeksi..lopetusindeksi], missä aloitusindeksi on slicen ensimmäinen sijainti ja lopetusindeksi on yksi enemmän kuin slicen viimeinen sijainti. Sisäisesti slice-tietorakenne tallentaa aloitussijainnin ja slicen pituuden, joka vastaa arvoa lopetusindeksi miinus aloitusindeksi. Eli tapauksessa let world = &s[6..11]; world olisi slice, joka sisältää osoittimen s-arvon indeksin 6 tavuun pituusarvolla 5.

Kuva 4-7 näyttää tämän kaaviona.

Three tables: a table representing the stack data of s, which points
to the byte at index 0 in a table of the string data "hello world" on
the heap. The third table rep-resents the stack data of the slice world, which
has a length value of 5 and points to byte 6 of the heap data table.

Kuva 4-7: Merkkijonoslice viittaa osaan String-arvosta

Rustin ..-välisyntaksilla, jos haluat aloittaa indeksistä 0, voit jättää pois arvon kahden pisteen edestä. Toisin sanoen, nämä ovat yhtä suuret:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

Samoin, jos sliceesi sisältää String-arvon viimeisen tavun, voit jättää pois loppunumeron. Tämä tarkoittaa, että nämä ovat yhtä suuret:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

Voit myös jättää pois molemmat arvot ottaaksesi slicen koko merkkijonosta. Eli nämä ovat yhtä suuret:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

Huom: Merkkijonoslicen väli-indeksien täytyy olla kelvollisilla UTF-8-merkki rajoilla. Jos yrität luoda merkkijonoslicen monitavuisen merkin keskeltä, ohjelmasi päättyy virheeseen. Merkkijonoslicien esittelyä varten oletamme tässä osiossa vain ASCII-merkkejä; UTF-8-käsittelyn tarkempi käsittely on Luvun 8 osiossa “UTF-8-koodatun tekstin tallentaminen merkkijonoilla”.

Kaiken tämän tiedon valossa kirjoitetaan first_word uudelleen palauttamaan slice. Tyyppi, joka merkitsee “merkkijonoslicea”, kirjoitetaan muodossa &str:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-18-first-word-slice/src/main.rs:here}}
}

Saamme sanan lopun indeksin samalla tavalla kuin Listauksessa 4-7, etsimällä ensimmäisen välilyönnin esiintymän. Kun löydämme välilyönnin, palautamme merkkijonoslicen käyttämällä merkkijonon alkua ja välilyönnin indeksiä aloitus- ja lopetusindekseinä.

Nyt kun kutsumme first_word-funktiota, saamme takaisin yhden arvon, joka on sidottu taustalla olevaan dataan. Arvo koostuu viitteestä slicen aloituspisteeseen ja slicen elementtien määrästä.

Slicen palauttaminen toimisi myös second_word-funktiolle:

fn second_word(s: &String) -> &str {

Meillä on nyt suoraviivainen API, jota on paljon vaikeampi sotkea, koska kääntäjä varmistaa, että String-arvoon tehdyt viitteet pysyvät kelvollisina. Muista bugi Listauksen 4-8 ohjelmassa, kun saimme ensimmäisen sanan lopun indeksin mutta sitten tyhjensimme merkkijonon, jolloin indeksimme oli virheellinen? Tuo koodi oli loogisesti virheellinen, mutta se ei näyttänyt välittömiä virheitä. Ongelmat ilmaantuisivat myöhemmin, jos jatkaisimme ensimmäisen sanan indeksin käyttöä tyhjän merkkijonon kanssa. Slicet tekevät tästä bugista mahdottoman ja kertovat meille ongelmasta koodissamme paljon aikaisemmin. first_word-funktion slice-version käyttö aiheuttaa käännösajan virheen:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/src/main.rs:here}}

Tässä on kääntäjän virhe:

{{#include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/output.txt}}

Muista lainausperiaatteista, että jos meillä on muuttumaton viite johonkin, emme voi myöskään ottaa muuttuvaa viitettä. Koska clear täytyy katkaista String, sen täytyy saada muuttuva viite. clear-kutsun jälkeinen println! käyttää viitettä muuttujassa word, joten muuttumattoman viitteen täytyy olla edelleen aktiivinen tuossa vaiheessa. Rust estää muuttuvan viitteen clear-funktiossa ja muuttumattoman viitteen muuttujassa word olemasta samaan aikaan, ja käännös epäonnistuu. Rust ei ole ainoastaan tehnyt API:stamme helpomman käyttää, vaan se on myös poistanut kokonaisen virheluokan käännösaikana!

Merkkijonoliteraalit sliceina

Muista, että puhuimme merkkijonoliteraalien tallentumisesta binääritiedostoon. Nyt kun tiedämme sliceista, voimme ymmärtää merkkijonoliteraalit oikein:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Tässä s-muuttujan tyyppi on &str: se on slice, joka osoittaa kyseiseen kohtaan binääritiedostossa. Tämän takia merkkijonoliteraalit ovat myös muuttumattomia; &str on muuttumaton viite.

Merkkijonoslicet parametreina

Tieto siitä, että voit ottaa sliceja literaaleista ja String-arvoista, johtaa meidät vielä yhteen parannukseen first_word-funktioon, ja se on sen signatuuri:

fn first_word(s: &String) -> &str {

Kokeneempi rustilainen kirjoittaisi Listauksessa 4-9 näytetyn signatuurin sen sijaan, koska se antaa meille mahdollisuuden käyttää samaa funktiota sekä &String- että &str-arvoilla.

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-09/src/main.rs:here}}
Listing 4-9: first_word-funktion parantaminen käyttämällä merkkijonoslicea s-parametrin tyypinä

Jos meillä on merkkijonoslice, voimme välittää sen suoraan. Jos meillä on String, voimme välittää slicen String-arvosta tai viitteen String-arvoon. Tämä joustavuus hyödyntää deref-pakotuksia, ominaisuutta, jota käsittelemme Luvun 15 osiossa “Implisiittiset deref-pakotukset funktioiden ja metodien kanssa”.

Funktion määrittely ottamaan merkkijonoslice String-viitteen sijaan tekee API:stamme yleisemmän ja hyödyllisemmän menettämättä mitään toiminnallisuutta:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-09/src/main.rs:usage}}
}

Muut slicet

Merkkijonoslicet, kuten saatat kuvitella, ovat merkkijonoihin erityisiä. Mutta on olemassa yleisempikin slice-tyyppi. Harkitse tätä taulukkoa:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Aivan kuten saatamme haluta viitata osaan merkkijonosta, saatamme haluta viitata osaan taulukosta. Teemme sen näin:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

Tällä slicellä on tyyppi &[i32]. Se toimii samalla tavalla kuin merkkijonoslicet, tallentamalla viitteen ensimmäiseen elementtiin ja pituuden. Käytät tämänlaista slicea kaikenlaisissa muissa kokoelmissa. Käsittelemme näitä kokoelmia yksityiskohtaisesti, kun puhumme vektoreista Luvussa 8.

Yhteenveto

Omistajuuden, lainauksen ja slicejen käsitteet varmistavat muistiturvallisuuden Rust- ohjelmissa käännösaikana. Rust-kieli antaa sinulle hallinnan muistin käytöstä samalla tavalla kuin muut järjestelmäohjelmointikielet, mutta se, että datan omistaja siivoaa automaattisesti datan, kun omistaja poistuu näkyvyysalueelta, tarkoittaa, että sinun ei tarvitse kirjoittaa ja debugata ylimääräistä koodia saadaksesi tämän hallinnan.

Omistajuus vaikuttaa siihen, miten monet muut Rustin osat toimivat, joten puhumme näistä käsitteistä lisää koko kirjan ajan. Siirrytään Lukuun 5 ja katsotaan, miten datan osia ryhmitellään yhteen struct-rakenteessa.

Rakenteiden (Structs) käyttö tietojen ryhmittelyyn

Rakenne (struct) on mukautettu tietotyyppi, jonka avulla voit paketoida yhteen ja nimetä useita toisiinsa liittyviä arvoja, jotka muodostavat merkityksellisen kokonaisuuden. Jos olet tuttu oliopohjaisista kielistä, struct vastaa olion tietokenttiä. Tässä luvussa vertailemme tuplia ja rakenteita syventäen aiempaa osaamistasi ja osoitamme, milloin rakenteet ovat parempi tapa ryhmitellä tietoa.

Käymme läpi, miten määritellään ja luodaan rakenteita. Lisäksi käsittelemme, kuinka määritellä rakenteeseen liittyviä funktioita, erityisesti metodeja, joiden avulla voidaan määrittää rakenteeseen liittyvää toimintaa. Rakenteet ja luettelotyypit (enums, käsitellään luvussa 6) ovat keskeisiä rakennuspalikoita, joilla voit luoda uusia tyyppejä ohjelmasi sovellusalueelle ja hyödyntää Rustin käännösaikaista tyyppitarkistusta mahdollisimman tehokkaasti.

Rakenteiden määrittäminen ja instanssien luominen

Rakenteiden määrittely ja instanssien luominen

Rakenteet ovat samankaltaisia kuin tuplat, joita käsiteltiin ”Tuplatyyppi” -osiossa, siinä mielessä, että molemmat pitävät useita toisiinsa liittyviä arvoja. Kuten tuplissa, rakenteen osat voivat olla eri tyyppejä. Toisin kuin tuplissa, rakenteessa nimeät jokaisen datan osan, jotta arvojen merkitys on selvä. Näiden nimien lisääminen tarkoittaa, että rakenteet ovat joustavampia kuin tuplat: sinun ei tarvitse luottaa datan järjestykseen määrittääksesi tai käyttääksesi instanssin arvoja.

Määrittääksesi rakenteen, kirjoitat avainsanan struct ja nimeät koko rakenteen. Rakenteen nimen pitäisi kuvata yhdessä ryhmiteltyjen datan osien merkitystä. Sitten aaltosulkeiden sisällä määrittelemme datan osien nimet ja tyypit, joita kutsumme kentiksi. Esimerkiksi Listausta 5-1 näyttää rakenteen, joka tallentaa tietoja käyttäjätilistä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-01/src/main.rs:here}}
}
Listing 5-1: User-rakenteen määrittely

Käyttääksemme rakennetta sen määrittelyn jälkeen, luomme kyseisen rakenteen instanssin määrittämällä konkreettiset arvot jokaiselle kentälle. Luomme instanssin ilmoittamalla rakenteen nimen ja lisäämällä aaltosulkeet, jotka sisältävät avain: arvo -pareja, joissa avaimet ovat kenttien nimiä ja arvot ovat dataa, jonka haluamme tallentaa näihin kenttiin. Emme tarvitse määrittää kenttiä samassa järjestyksessä, jossa ilmoitimme ne rakenteessa. Toisin sanoen rakenteen määrittely on kuin yleinen malli tyypille, ja instanssit täyttävät mallin tietyllä datalla luodakseen tyypin arvoja. Esimerkiksi voimme ilmoittaa tietyn käyttäjän kuten Listauksessa 5-2 on esitetty.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-02/src/main.rs:here}}
}
Listing 5-2: User-rakenteen instanssin luominen

Saadaksemme tietyn arvon rakenteesta, käytämme pistesyntaksia. Esimerkiksi käyttääksemme tämän käyttäjän sähköpostiosoitetta, käytämme user1.email. Jos instanssi on muuttuva, voimme muuttaa arvoa käyttämällä pistesyntaksia ja sijoittamalla tiettyyn kenttään. Listausta 5-3 näyttää, miten muutetaan email-kentän arvoa muuttuvassa User-instanssissa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-03/src/main.rs:here}}
}
Listing 5-3: email-kentän arvon muuttaminen User-instanssissa

Huomaa, että koko instanssin täytyy olla muuttuva; Rust ei salli meidän merkitä vain tiettyjä kenttiä muuttuviksi. Kuten minkä tahansa lausekkeen kanssa, voimme rakentaa uuden instanssin rakenteesta funktion rungon viimeisenä lausekkeena palauttaaksemme implisiittisesti kyseisen uuden instanssin.

Listausta 5-4 näyttää build_user-funktion, joka palauttaa User-instanssin annetulla sähköpostilla ja käyttäjänimellä. active-kenttä saa arvon true, ja sign_in_count saa arvon 1.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-04/src/main.rs:here}}
}
Listing 5-4: build_user-funktio, joka ottaa sähköpostin ja käyttäjänimen ja palauttaa User-instanssin

On järkevää nimetä funktion parametrit samalla nimellä kuin rakenteen kentät, mutta email- ja username-kenttien ja -muuttujien toistaminen on hieman työlästä. Jos rakenteella olisi enemmän kenttiä, jokaisen nimen toistaminen olisi vieläkin ärsyttävämpää. Onneksi on kätevä lyhenne!

Kentän alustuksen lyhenteen käyttö

Koska parametrien nimet ja rakenteen kenttien nimet ovat täsmälleen samat Listauksessa 5-4, voimme käyttää kentän alustuksen lyhenne-syntaksia kirjoittaaksemme build_user-funktion uudelleen niin, että se käyttäytyy täsmälleen samalla tavalla, mutta ilman username- ja email-toistoa, kuten Listauksessa 5-5 on esitetty.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-05/src/main.rs:here}}
}
Listing 5-5: build_user-funktio, joka käyttää kentän alustuksen lyhennettä, koska username- ja email-parametreilla on sama nimi kuin rakenteen kentillä

Tässä luomme uuden User-rakenteen instanssin, jolla on kenttä nimeltä email. Haluamme asettaa email-kentän arvoksi build_user-funktion email-parametrin arvon. Koska email-kentällä ja email-parametrilla on sama nimi, meidän tarvitsee kirjoittaa vain email eikä email: email.

Instanssien luominen toisista instansseista rakenteen päivityssyntaksilla

On usein hyödyllistä luoda uusi instanssi rakenteesta, joka sisältää suurimman osan arvoista toisesta instanssista, mutta muuttaa joitakin. Voit tehdä tämän käyttämällä rakenteen päivityssyntaksia.

Ensin Listauksessa 5-6 näytämme, miten luodaan uusi User-instanssi user2:ssa tavallisesti ilman päivityssyntaksia. Asetamme uuden arvon email-kentälle, mutta käytämme muuten samoja arvoja user1:stä, jonka loimme Listauksessa 5-2.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-06/src/main.rs:here}}
}
Listing 5-6: Uuden User-instanssin luominen käyttäen kaikkia paitsi yhtä arvoa user1:stä

Käyttämällä rakenteen päivityssyntaksia voimme saavuttaa saman vaikutuksen vähemmällä koodilla, kuten Listauksessa 5-7 on esitetty. ..-syntaksi määrittää, että jäljellä olevilla kentillä, joita ei ole eksplisiittisesti asetettu, pitäisi olla sama arvo kuin annetun instanssin kentillä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-07/src/main.rs:here}}
}
Listing 5-7: Rakenteen päivityssyntaksin käyttö uuden email-arvon asettamiseksi User-instanssille, mutta muiden arvojen käyttämiseksi user1:stä

Listauksen 5-7 koodi luo myös instanssin user2:ssa, jolla on eri arvo email-kentälle, mutta samat arvot username-, active- ja sign_in_count-kentille user1:stä. ..user1 täytyy tulla viimeisenä määrittämään, että jäljellä olevat kentät saavat arvonsa user1:n vastaavista kentistä, mutta voimme valita määrittää arvoja niin monelle kentälle kuin haluamme missä tahansa järjestyksessä riippumatta kenttien järjestyksestä rakenteen määrittelyssä.

Huomaa, että rakenteen päivityssyntaksi käyttää =-merkkiä kuten sijoituslauseke; tämä johtuu siitä, että se siirtää datan, kuten näimme ”Muuttujat ja data vuorovaikutuksessa siirron kanssa” -osiossa. Tässä esimerkissä emme voi enää käyttää user1:tä user2:n luomisen jälkeen, koska user1:n username-kentän String siirrettiin user2:een. Jos olisimme antaneet user2:lle uudet String-arvot sekä email- että username-kentille, ja siten käyttäneet vain active- ja sign_in_count-arvoja user1:stä, user1 olisi edelleen kelvollinen user2:n luomisen jälkeen. Sekä active että sign_in_count ovat tyyppejä, jotka toteuttavat Copy-traitin, joten ”Vain pinossa oleva data: Copy” -osiossa käsittelemämme käyttäytyminen pätee. Voimme edelleen käyttää user1.email tässä esimerkissä, koska sen arvoa ei siirretty pois.

Tuple-rakenteiden käyttö nimeämättömillä kentillä eri tyyppien luomiseksi

Rust tukee myös rakenteita, jotka näyttävät samankaltaisilta kuin tuplat, ja joita kutsutaan tuple-rakenteiksi. Tuple-rakenteilla on rakenteen nimen tuoma lisämerkitys, mutta niillä ei ole nimiä liitettynä kenttiinsä; niillä on vain kenttien tyypit. Tuple-rakenteet ovat hyödyllisiä, kun haluat antaa koko tuplalle nimen ja tehdä tuplasta eri tyypin kuin muista tuplista, ja kun jokaisen kentän nimeäminen kuten tavallisessa rakenteessa olisi puhelias tai tarpeeton.

Määrittääksesi tuple-rakenteen, aloita struct-avainsanalla ja rakenteen nimellä, jota seuraa tuplan tyypit. Esimerkiksi tässä määrittelemme ja käytämme kahta tuple-rakennetta nimeltä Color ja Point:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-01-tuple-structs/src/main.rs}}
}

Huomaa, että black- ja origin-arvot ovat eri tyyppejä, koska ne ovat eri tuple-rakenteiden instansseja. Jokainen määrittelemäsi rakenne on oma tyypinsä, vaikka rakenteen kentät saattaisivat olla samoja tyyppejä. Esimerkiksi funktio, joka ottaa parametrin tyypillä Color, ei voi ottaa Point-argumenttia, vaikka molemmat tyypit koostuvat kolmesta i32-arvosta. Muuten tuple-rakenneinstanssit ovat samankaltaisia kuin tuplat siinä mielessä, että voit purkaa ne yksittäisiin osiinsa, ja voit käyttää .-merkkiä, jota seuraa indeksi, päästäksesi yksittäiseen arvoon. Toisin kuin tuplat, tuple-rakenteet vaativat sinua nimeämään rakenteen tyypin, kun purat ne. Esimerkiksi kirjoittaisimme let Point(x, y, z) = point.

Yksikkömäiset rakenteet ilman kenttiä

Voit myös määritellä rakenteita, joilla ei ole lainkaan kenttiä! Näitä kutsutaan yksikkömäisiksi rakenteiksi, koska ne käyttäytyvät samankaltaisesti kuin (), yksikkötyyppi, josta mainitsimme ”Tuplatyyppi” -osiossa. Yksikkömäiset rakenteet voivat olla hyödyllisiä, kun sinun täytyy toteuttaa trait jollekin tyypille, mutta sinulla ei ole dataa, jonka haluaisit tallentaa itse tyyppiin. Käsittelemme traitteja Luvussa 10. Tässä on esimerkki yksikkörakenteen nimeltä AlwaysEqual ilmoittamisesta ja instanssin luomisesta:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-04-unit-like-structs/src/main.rs}}
}

Määrittääksemme AlwaysEqual:in, käytämme struct-avainsanaa, haluamaamme nimeä ja sitten puolipistettä. Ei tarvetta aaltosulkeille tai sulkeille! Sitten voimme saada AlwaysEqual-instanssin subject-muuttujaan samalla tavalla: käyttämällä määrittelemäämme nimeä ilman aaltosulkeita tai sulkeita. Kuvittele, että myöhemmin toteutamme käyttäytymistä tälle tyypille siten, että jokainen AlwaysEqual-instanssi on aina yhtä suuri kuin minkä tahansa muun tyypin instanssi, ehkä testaustarkoituksiin tunnetun tuloksen saamiseksi. Emme tarvitsisi dataa toteuttaaksemme tuota käyttäytymistä! Näet Luvussa 10, miten määritellä traitteja ja toteuttaa ne mille tahansa tyypille, mukaan lukien yksikkömäiset rakenteet.

Rakenteen datan omistajuus

User-rakenteen määrittelyssä Listauksessa 5-1 käytimme omistettua String-tyyppiä &str-merkkijonoviipaletyypin sijaan. Tämä on tarkoituksellinen valinta, koska haluamme jokaisen tämän rakenteen instanssin omistavan kaiken datansa ja datan olevan kelvollinen niin kauan kuin koko rakenne on kelvollinen.

On myös mahdollista, että rakenteet tallentavat viittauksia muualla omistettuun dataan, mutta se vaatii elinaikojen käyttöä, Rust-ominaisuutta, jota käsittelemme Luvussa 10. Elinaika varmistavat, että rakenteen viittaama data on kelvollinen niin kauan kuin rakenne on. Sanotaan, että yrität tallentaa viittauksen rakenteeseen määrittämättä elinaikoja, kuten seuraavassa; tämä ei toimi:

Filename: src/main.rs
struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

Kääntäjä valittaa, että se tarvitsee elinaikamäärittimet:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

Luvussa 10 käsittelemme, miten korjata nämä virheet, jotta voit tallentaa viittauksia rakenteisiin, mutta toistaiseksi korjaamme tällaiset virheet käyttämällä omistettuja tyyppejä kuten String viittausten kuten &str sijaan.

Esimerkkiohjelma rakenteiden käytöstä

Esimerkkiohjelma, joka käyttää rakenteita

Ymmärtääksemme, milloin rakenteita kannattaa käyttää, kirjoitamme ohjelman, joka laskee suorakulmion pinta-alan. Aloitamme käyttämällä yksittäisiä muuttujia ja refaktoroimme ohjelman vähitellen niin, että käytämme rakenteita.

Tehdään uusi binääriprojekti Cargo-ohjelmalla nimeltä rectangles, joka ottaa suorakulmion leveyden ja korkeuden pikseleinä ja laskee suorakulmion pinta-alan. Listausta 5-8 näyttää lyhyen ohjelman, joka tekee juuri tämän projektimme src/main.rs-tiedostossa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/src/main.rs:all}}
}
Listing 5-8: Suorakulmion pinta-alan laskeminen erillisillä leveys- ja korkeusmuuttujilla

Aja ohjelma komennolla cargo run:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/output.txt}}

Tämä koodi onnistuu laskemaan suorakulmion pinta-alan kutsumalla area-funktiota kummallakin mitalla, mutta voimme tehdä koodista selkeämmän ja luettavamman.

Ongelma tässä koodissa näkyy area-funktion signatuurissa:

{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/src/main.rs:here}}

area-funktion pitäisi laskea yhden suorakulmion pinta-ala, mutta kirjoittamassamme funktiossa on kaksi parametria, eikä ohjelmassamme ole missään selvää, että parametrit liittyvät toisiinsa. Olisi luettavampaa ja hallittavampaa ryhmitellä leveys ja korkeus yhteen. Olemme jo käsitelleet yhden tavan tehdä tämä Luvun 3 ”Tuplatyyppi” -osiossa: käyttämällä tuplia.

Refaktorointi tuplilla

Listausta 5-9 näyttää toisen version ohjelmastamme, joka käyttää tuplia.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-09/src/main.rs}}
}
Listing 5-9: Suorakulmion leveyden ja korkeuden määrittely tuplalla

Eräästä näkökulmasta tämä ohjelma on parempi. Tuplat antavat lisätä hieman rakennetta, ja välitämme nyt vain yhden argumentin. Mutta toisesta näkökulmasta tämä versio on epäselvempi: tuplat eivät nimeä elementtejään, joten meidän täytyy indeksoida tuplan osia, mikä tekee laskennastamme vähemmän ilmeistä.

Leveyden ja korkeuden sekoittaminen ei vaikuttaisi pinta-alan laskentaan, mutta jos haluaisimme piirtää suorakulmion ruudulle, se vaikuttaisi! Meidän piti muistaa, että width on tuplan indeksi 0 ja height on tuplan indeksi 1. Tämä olisi vielä vaikeampaa jollekin muulle selvittää ja muistaa, jos hän käyttäisi koodiamme. Koska emme välittäneet datamme merkitystä koodissamme, virheiden tekeminen on nyt helpompaa.

Refaktorointi rakenteilla

Käytämme rakenteita lisätäksemme merkitystä nimeämällä datan. Voimme muuntaa käyttämämme tuplan rakenteeksi, jolla on nimi kokonaisuudelle sekä nimet osille, kuten Listauksessa 5-10 on esitetty.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-10/src/main.rs}}
}
Listing 5-10: Rectangle-rakenteen määrittely

Tässä olemme määritelleet rakenteen ja nimenneet sen Rectangle. Aaltosulkeiden sisällä määritimme kentät width ja height, joilla molemmilla on tyyppi u32. Sitten main-funktiossa loimme tietyn Rectangle-instanssin, jonka leveys on 30 ja korkeus 50.

area-funktiomme on nyt määritelty yhdellä parametrilla, jonka olemme nimenneet rectangle ja jonka tyyppi on Rectangle-rakenteen instanssin muuttumaton lainaus. Kuten mainittiin Luvussa 4, haluamme lainata rakenteen omistajuuden siirtämisen sijaan. Näin main säilyttää omistajuutensa ja voi jatkaa rect1:n käyttöä, minkä vuoksi käytämme &-merkkiä funktion signatuurissa ja funktiokutsussa.

area-funktio käyttää Rectangle-instanssin width- ja height-kenttiä (huomaa, että lainatun rakenteen instanssin kenttien käyttö ei siirrä kenttien arvoja, minkä vuoksi näet usein rakenteiden lainauksia). area-funktion signatuuri sanoo nyt täsmälleen, mitä tarkoitamme: laske Rectangle-rakenteen pinta-ala käyttämällä sen width- ja height-kenttiä. Tämä välittää, että leveys ja korkeus liittyvät toisiinsa, ja antaa kuvaavat nimet arvoille tuplan indeksiarvojen 0 ja 1 sijaan. Tämä on selkeyden voitto.

Toiminnallisuuden lisääminen johdettavilla traitteilla

Olisi hyödyllistä pystyä tulostamaan Rectangle-instanssi debugatessamme ohjelmaamme ja näkemään kaikkien sen kenttien arvot. Listausta 5-11 yrittää käyttää println!-makroa kuten aiemmissa luvuissa. Tämä ei kuitenkaan toimi.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/src/main.rs}}
Listing 5-11: Yritys tulostaa Rectangle-instanssi

Kun käännämme tämän koodin, saamme virheen, jonka ydinviesti on:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/output.txt:3}}

println!-makro voi tehdä monenlaista muotoilua, ja oletusarvoisesti aaltosulkeet kertovat println!:lle käyttämään Display-nimistä muotoilua: tulostusta, joka on tarkoitettu suoraan loppukäyttäjälle. Tähän mennessä näkemämme primitiivityypit toteuttavat Display-traitin oletusarvoisesti, koska on vain yksi tapa näyttää 1 tai mikä tahansa muu primitiivityyppi käyttäjälle. Mutta rakenteiden kohdalla se, miten println!:n pitäisi muotoilla tuloste, on vähemmän selvä, koska näyttömahdollisuuksia on enemmän: haluatko pilkkuja vai et? Haluatko tulostaa aaltosulkeet? Pitäisikö kaikki kentät näyttää? Tämän epäselvyyden vuoksi Rust ei yritä arvata, mitä haluamme, eikä rakenteilla ole valmista Display-toteutusta käytettäväksi println!:n ja {}-paikkamerkin kanssa.

Jos jatkamme virheilmoitusten lukemista, löydämme tämän hyödyllisen huomautuksen:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/output.txt:9:10}}

Kokeillaan! println!-makrokutsu näyttää nyt tältä: println!("rect1 is {rect1:?}");. Määritteen :? laittaminen aaltosulkeiden sisään kertoo println!:lle, että haluamme käyttää Debug-nimistä tulostusmuotoa. Debug-traitin avulla voimme tulostaa rakenteemme tavalla, joka on hyödyllinen kehittäjille, jotta näemme sen arvon debugatessamme koodiamme.

Käännä koodi tällä muutoksella. Pah! Saamme silti virheen:

{{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-01-debug/output.txt:3}}

Mutta kääntäjä antaa taas hyödyllisen huomautuksen:

{{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-01-debug/output.txt:9:10}}

Rust sisältää toiminnallisuuden debug-tietojen tulostamiseen, mutta meidän täytyy erikseen valita se käyttöön rakenteellemme. Teemme sen lisäämällä ulkoisen attribuutin #[derive(Debug)] juuri ennen rakenteen määrittelyä, kuten Listauksessa 5-12 on esitetty.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-12/src/main.rs}}
}
Listing 5-12: Attribuutin lisääminen Debug-traitin johdattamiseen ja Rectangle-instanssin tulostaminen debug-muotoilulla

Nyt kun ajamme ohjelman, emme saa virheitä ja näemme seuraavan tulosteen:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-12/output.txt}}

Hienoa! Tuloste ei ole kaunein mahdollinen, mutta se näyttää kaikkien tämän instanssin kenttien arvot, mikä auttaisi varmasti debuggauksessa. Kun meillä on suurempia rakenteita, on hyödyllistä saada hieman helpommin luettava tuloste; näissä tapauksissa voimme käyttää {:#?} muodon {:?} sijaan println!-merkkijonossa. Tässä esimerkissä {:#?}-tyyli tuottaa seuraavan tulosteen:

{{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-02-pretty-debug/output.txt}}

Toinen tapa tulostaa arvo Debug-muodossa on käyttää dbg!-makroa, joka ottaa omistajuuden lausekkeesta (toisin kuin println!, joka ottaa viittauksen), tulostaa tiedoston ja rivinumeron, jossa dbg!-makrokutsu tapahtuu koodissasi, sekä kyseisen lausekkeen tulosarvon, ja palauttaa lausekkeen omistajuuden.

Huom: dbg!-makrokutsu tulostaa vakiovirhevirtaan (stderr), toisin kuin println!, joka tulostaa vakiotulostusvirtaan (stdout). Puhumme stderr:stä ja stdout:sta lisää Luvun 12 osiossa ”Virheiden ohjaaminen vakiovirheeseen”.

Tässä on esimerkki, jossa olemme kiinnostuneita arvosta, joka sijoitetaan width-kenttään, sekä koko rakenteen arvosta rect1:ssä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-05-dbg-macro/src/main.rs}}
}

Voimme laittaa dbg!:n lausekkeen 30 * scale ympärille, ja koska dbg! palauttaa lausekkeen arvon omistajuuden, width-kenttä saa saman arvon kuin ilman dbg!-kutsua. Emme halua dbg!:n ottavan rect1:n omistajuutta, joten käytämme viittausta rect1:een seuraavassa kutsussa. Tässä on tämän esimerkin tuloste:

{{#include ../listings/ch05-using-structs-to-structure-related-data/no-listing-05-dbg-macro/output.txt}}

Näemme, että ensimmäinen tuloste tuli src/main.rs-tiedoston riviltä 10, jossa debuggaamme lauseketta 30 * scale, ja sen tulosarvo on 60 (Debug-muotoilu kokonaisluvuille tulostaa vain niiden arvon). dbg!-kutsu src/main.rs-tiedoston rivillä 14 tulostaa arvon &rect1, joka on Rectangle-rakenne. Tämä tuloste käyttää Rectangle-tyypin siistiä Debug-muotoilua. dbg!-makro voi olla todella hyödyllinen selvittäessäsi, mitä koodisi tekee!

Debug-traitin lisäksi Rust on tarjonnut joukon traitteja, joita voimme käyttää derive-attribuutin kanssa lisätäksemme hyödyllistä käyttäytymistä mukautetuille tyypeillemme. Nämä traitit ja niiden käyttäytymiset on lueteltu Liitteessä C. Käsittelemme, miten toteutamme nämä traitit mukautetulla käyttäytymisellä sekä miten luomme omia traitteja Luvussa 10. On myös monia muita attribuutteja kuin derive; lisätietoja on Rust-viitteen ”Attribuutit”-osiossa.

area-funktiomme on hyvin spesifinen: se laskee vain suorakulmioiden pinta-alan. Olisi hyödyllistä sitoa tämä käyttäytyminen tiukemmin Rectangle-rakenteeseemme, koska se ei toimi minkään muun tyypin kanssa. Katsotaan, miten voimme jatkaa tämän koodin refaktorointia muuttamalla area-funktion area-metodiksi, joka on määritelty Rectangle-tyypillemme.

Metodisyntaksi

Metodisyntaksi

Metodit ovat samankaltaisia kuin funktiot: ne määritellään fn-avainsanalla ja nimellä, niillä voi olla parametreja ja palautusarvo, ja ne sisältävät koodia, joka suoritetaan, kun metodia kutsutaan jostakin muualta. Toisin kuin funktiot, metodit määritellään rakenteen (tai luettelotyypin tai trait-objektin, joita käsitellään Luvussa 6 ja Luvussa 18, vastaavasti) yhteydessä, ja niiden ensimmäinen parametri on aina self, joka edustaa rakenteen instanssia, jolle metodia kutsutaan.

Metodin määrittely

Muutetaan area-funktio, jolla on Rectangle-instanssi parametrina, ja tehdään sen sijaan area-metodi, joka määritellään Rectangle-rakenteelle, kuten Listauksessa 5-13 on esitetty.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-13/src/main.rs}}
}
Listing 5-13: area-metodin määrittely Rectangle-rakenteelle

Määrittääksemme funktion Rectangle-kontekstissa, aloitamme impl (toteutus) -lohkon Rectangle-rakenteelle. Kaikki tämän impl-lohkon sisällä on liitetty Rectangle-tyyppiin. Siirrämme sitten area-funktion impl-lohkon aaltosulkeiden sisään ja muutamme ensimmäisen (ja tässä tapauksessa ainoan) parametrin signatuurissa ja kaikkialla rungossa muotoon self. main-funktiossa, jossa kutsuimme area-funktiota ja välitimme rect1:n argumenttina, voimme sen sijaan käyttää metodisyntaksia kutsuaksemme area-metodia Rectangle-instanssillamme. Metodisyntaksi tulee instanssin jälkeen: lisäämme pisteen, metodin nimen, sulkeet ja mahdolliset argumentit.

area-metodin signatuurissa käytämme &self:ää rectangle: &Rectangle:n sijaan. &self on itse asiassa lyhenne muodolle self: &Self. impl-lohkon sisällä tyyppi Self on alias tyypille, jolle impl-lohko on tarkoitettu. Metodeilla täytyy olla ensimmäisenä parametrinaan nimeltään self tyyppiä Self, joten Rust antaa sinun lyhentää tämän pelkällä nimellä self ensimmäisessä parametripaikassa. Huomaa, että meidän täytyy silti käyttää &:ää self-lyhenteen edessä osoittaaksemme, että tämä metodi lainaa Self-instanssia, aivan kuten teimme rectangle: &Rectangle:ssa. Metodit voivat ottaa omistajuuden self:stä, lainata self:ää muuttumattomasti, kuten teimme tässä, tai lainata self:ää muuttuvasti, aivan kuten ne voivat tehdä mille tahansa muulle parametrille.

Valitsimme &self:n tässä samasta syystä kuin käytimme &Rectangle:a funktioversiossa: emme halua ottaa omistajuutta, ja haluamme vain lukea rakenteen dataa, emme kirjoittaa siihen. Jos haluaisimme muuttaa instanssia, jolle metodia kutsutaan, osana sitä, mitä metodi tekee, käyttäisimme &mut self:ää ensimmäisenä parametrina. Metodi, joka ottaa omistajuuden instanssista käyttämällä pelkkää self:ää ensimmäisenä parametrina, on harvinainen; tätä tekniikkaa käytetään yleensä silloin, kun metodi muuntaa self:n joksikin muuksi ja haluat estää kutsujaa käyttämästä alkuperäistä instanssia muunnoksen jälkeen.

Pääsyy metodien käyttämiseen funktioiden sijaan, metodisyntaksin tarjoamisen ja sen lisäksi, ettei self:n tyyppiä tarvitse toistaa jokaisen metodin signatuurissa, on organisointi. Olemme sijoittaneet kaikki asiat, joita voimme tehdä tyypin instanssilla, yhteen impl-lohkoon sen sijaan, että pakottaisimme koodimme tulevat käyttäjät etsimään Rectangle:n ominaisuuksia eri paikoista tarjoamassamme kirjastossa.

Huomaa, että voimme halutessamme antaa metodille saman nimen kuin rakenteen kentällä on. Esimerkiksi voimme määritellä Rectangle-rakenteelle metodin, joka on myös nimeltään width:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-06-method-field-interaction/src/main.rs:here}}
}

Tässä valitsemme, että width-metodi palauttaa true, jos instanssin width- kentän arvo on suurempi kuin 0, ja false, jos arvo on 0: voimme käyttää kenttää saman nimisen metodin sisällä mihin tahansa tarkoitukseen. main-funktiossa, kun rect1.width:n jälkeen tulee sulkeet, Rust tietää, että tarkoitamme metodia width. Kun emme käytä sulkeita, Rust tietää, että tarkoitamme kenttää width.

Usein, mutta ei aina, kun annamme metodille saman nimen kuin kentällä on, haluamme sen vain palauttavan kentän arvon eikä tekevän mitään muuta. Tällaisia metodeja kutsutaan gettereiksi, eikä Rust toteuta niitä automaattisesti rakenteen kentille, kuten jotkut muut kielet tekevät. Getterit ovat hyödyllisiä, koska voit tehdä kentästä yksityisen mutta metodista julkisen ja siten mahdollistaa vain-luku-pääsyn kyseiseen kenttään osana tyypin julkista API:a. Käsittelemme, mitä julkinen ja yksityinen ovat ja miten kenttä tai metodi merkitään julkiseksi tai yksityiseksi, Luvussa 7.

Missä on ->-operaattori?

C:ssä ja C++:ssa käytetään kahta eri operaattoria metodien kutsumiseen: käytät .:a, jos kutsut metodia suoraan objektille, ja ->:a, jos kutsut metodia objektin osoittimella ja sinun täytyy ensin dereferoida osoitin. Toisin sanoen, jos object on osoitin, object->something() on samankaltainen kuin (*object).something().

Rustilla ei ole vastaavaa ->-operaattoria; sen sijaan Rustissa on ominaisuus nimeltä automaattinen viittaaminen ja dereferointi. Metodien kutsuminen on yksi harvoista paikoista Rustissa, joissa tämä käyttäytyminen esiintyy.

Näin se toimii: kun kutsut metodia object.something():lla, Rust lisää automaattisesti &:n, &mut:n tai *:n, jotta object vastaa metodin signatuuria. Toisin sanoen seuraavat ovat samat:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

Ensimmäinen näyttää paljon siistimmältä. Tämä automaattinen viittaamisen käyttäytyminen toimii, koska metodeilla on selkeä vastaanottaja—self:n tyyppi. Vastaanottajan ja metodin nimen perusteella Rust voi selvästi päätellä, lukeeko metodi (&self), muuttaako (&mut self) vai kuluttaako (self) instanssia. Se, että Rust tekee lainaamisesta implisiittistä metodien vastaanottajille, on merkittävä osa omistajuuden käytännön ergonomiaa.

Metodit, joilla on useita parametreja

Harjoitellaan metodien käyttöä toteuttamalla toinen metodi Rectangle-rakenteelle. Tällä kertaa haluamme, että Rectangle-instanssi ottaa toisen Rectangle- instanssin ja palauttaa true, jos toinen Rectangle mahtuu kokonaan self:n (eli ensimmäisen Rectangle:n) sisään; muuten sen pitäisi palauttaa false. Toisin sanoen, kun olemme määritelleet can_hold-metodin, haluamme pystyä kirjoittamaan Listauksessa 5-14 näytetyn ohjelman.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-14/src/main.rs}}
Listing 5-14: Vielä kirjoittamattoman can_hold-metodin käyttö

Odotettu tuloste näyttäisi seuraavalta, koska rect2:n molemmat mitat ovat pienempiä kuin rect1:n mitat, mutta rect3 on leveämpi kuin rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Tiedämme, että haluamme määritellä metodin, joten se on impl Rectangle -lohkon sisällä. Metodin nimi on can_hold, ja se ottaa parametrina muuttumattoman lainauksen toisesta Rectangle:sta. Voimme päätellä parametrin tyypin katsomalla koodia, joka kutsuu metodia: rect1.can_hold(&rect2) välittää &rect2:n, joka on muuttumaton lainaus rect2:sta, Rectangle-instanssista. Tämä on järkevää, koska meidän täytyy vain lukea rect2:a (eikä kirjoittaa, mikä tarkoittaisi, että tarvitsisimme muuttuvan lainauksen), ja haluamme main:in säilyttävän omistajuuden rect2:sta, jotta voimme käyttää sitä uudelleen can_hold-metodin kutsumisen jälkeen. can_hold-metodin palautusarvo on totuusarvo, ja toteutus tarkistaa, ovatko self:n leveys ja korkeus suurempia kuin toisen Rectangle:n leveys ja korkeus, vastaavasti. Lisätään uusi can_hold-metodi Listauksen 5-13 impl-lohkoon, kuten Listauksessa 5-15 on esitetty.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-15/src/main.rs:here}}
}
Listing 5-15: can_hold-metodin toteutus Rectangle-rakenteelle, joka ottaa toisen Rectangle-instanssin parametrina

Kun ajamme tämän koodin Listauksen 5-14 main-funktiolla, saamme haluamamme tulosteen. Metodeilla voi olla useita parametreja, jotka lisätään signatuuriin self-parametrin jälkeen, ja nämä parametrit toimivat aivan kuten parametrit funktioissa.

Liittyvät funktiot

Kaikkia impl-lohkon sisällä määriteltyjä funktioita kutsutaan liittyviksi funktioiksi, koska ne liittyvät impl:n jälkeen nimettyyn tyyppiin. Voimme määritellä liittyviä funktioita, joilla ei ole self:ää ensimmäisenä parametrinaan (ja jotka siten eivät ole metodeja), koska niiden ei tarvitse tyypin instanssia työskennelläkseen. Olemme jo käyttäneet tällaista funktiota: String::from-funktiota, joka on määritelty String-tyypille.

Liittyviä funktioita, jotka eivät ole metodeja, käytetään usein konstruktoreina, jotka palauttavat rakenteen uuden instanssin. Näitä kutsutaan usein new:ksi, mutta new ei ole erityinen nimi eikä se ole sisäänrakennettu kieleen. Esimerkiksi voisimme halutessamme tarjota liittyvän funktion nimeltä square, jolla olisi yksi mittaparametri ja joka käyttäisi sitä sekä leveytenä että korkeutena, mikä helpottaisi neliön muotoisen Rectangle:n luomista sen sijaan, että joutuisimme määrittämään saman arvon kahdesti:

Tiedostonimi: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
}

Self-avainsanat palautustyypissä ja funktion rungossa ovat aliaksia tyypille, joka esiintyy impl-avainsanan jälkeen, joka tässä tapauksessa on Rectangle.

Kutsuaksemme tätä liittyvää funktiota, käytämme ::-syntaksia rakenteen nimen kanssa; let sq = Rectangle::square(3); on esimerkki. Tämä funktio on rakenteen nimiavaruudessa: ::-syntaksia käytetään sekä liittyville funktioille että moduulien luomille nimiavaruuksille. Käsittelemme moduuleja Luvussa 7.

Useita impl-lohkoja

Jokaisella rakenteella saa olla useita impl-lohkoja. Esimerkiksi Listausta 5-15 vastaa Listauksessa 5-16 näytetty koodi, jossa jokainen metodi on omassa impl-lohkossaan.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-16/src/main.rs:here}}
}
Listing 5-16: Listauksen 5-15 uudelleenkirjoitus useilla impl-lohkoilla

Ei ole syytä erotella näitä metodeja useisiin impl-lohkoihin tässä, mutta tämä on kelvollista syntaksia. Näemme tapauksen, jossa useat impl-lohkot ovat hyödyllisiä Luvussa 10, jossa käsittelemme geneerisiä tyyppejä ja traitteja.

Yhteenveto

Rakenteet antavat sinun luoda mukautettuja tyyppejä, jotka ovat merkityksellisiä sovellusalueellesi. Rakenteiden avulla voit pitää toisiinsa liittyvät datan osat yhteydessä toisiinsa ja nimetä jokaisen osan, jotta koodisi on selkeää. impl-lohkoissa voit määritellä tyypillesi liittyviä funktioita, ja metodit ovat eräänlaisia liittyviä funktioita, joiden avulla voit määrittää käyttäytymisen, joka rakenteidesi instansseilla on.

Mutta rakenteet eivät ole ainoa tapa luoda mukautettuja tyyppejä: siirrytään Rustin luettelotyyppi-ominaisuuteen lisätäksemme työkalupakkiisi uuden työkalun.

Luettelotyypit (Enums) ja mallintaminen

Tässä luvussa tarkastelemme luettelotyyppejä, joita kutsutaan myös enum-tyypeiksi. Enumit mahdollistavat tyypin määrittelyn luettelemalla sen mahdolliset variantit. Ensin määrittelemme ja käytämme enumia osoittaaksemme, kuinka enum voi tallentaa sekä merkitystä että dataa.

Seuraavaksi tutkimme erityisen hyödyllistä Option-enumia, joka ilmaisee, että arvo voi olla joko jotain tai ei mitään. Sitten perehdymme match-lausekkeeseen, joka tekee erilaisten enum-arvojen käsittelystä helppoa ja selkeää. Lopuksi käsittelemme if let -rakennetta, joka on toinen kätevä tapa käsitellä enum-arvoja yksinkertaisemmin.

Enumin määrittäminen

Enumin määrittely

Siinä missä rakenteet antavat tavan ryhmitellä toisiinsa liittyviä kenttiä ja dataa, kuten Rectangle-rakenteen width- ja height-kentät, enumit antavat tavan sanoa, että arvo on yksi mahdollisista arvoista. Esimerkiksi saatamme haluta sanoa, että Rectangle on yksi mahdollisista muodoista, joihin kuuluvat myös Circle ja Triangle. Tätä varten Rust antaa meidän koodata nämä mahdollisuudet enumina.

Katsotaan tilannetta, jonka haluamme ilmaista koodissa, ja nähdään, miksi enumit ovat hyödyllisiä ja sopivampia kuin rakenteet tässä tapauksessa. Oletetaan, että meidän täytyy työskennellä IP-osoitteiden kanssa. Tällä hetkellä IP-osoitteille käytetään kahta pääasiallista standardia: versio neljä ja versio kuusi. Koska nämä ovat ainoat mahdolliset IP-osoitteet, joita ohjelmamme kohtaa, voimme luetella kaikki mahdolliset variantit, mistä sana enumeration eli luettelotyyppi saa nimensä.

Mikä tahansa IP-osoite voi olla joko version neljä tai version kuusi osoite, mutta ei molempia samaan aikaan. Tämä IP-osoitteiden ominaisuus tekee enum-tietorakenteesta sopivan, koska enum-arvo voi olla vain yksi sen varianteista. Sekä version neljä että version kuusi osoitteet ovat silti pohjimmiltaan IP-osoitteita, joten niitä pitäisi käsitellä samana tyypinä, kun koodi käsittelee tilanteita, jotka koskevat mitä tahansa IP-osoitetyyppiä.

Voimme ilmaista tämän käsitteen koodissa määrittelemällä IpAddrKind-luettelon ja listaamalla mahdolliset IP-osoitteen tyypit, V4 ja V6. Nämä ovat enumin variantit:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:def}}
}

IpAddrKind on nyt mukautettu tietotyyppi, jota voimme käyttää muualla koodissamme.

Enum-arvot

Voimme luoda kummankin IpAddrKind-variantin instanssit näin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:instance}}
}

Huomaa, että enumin variantit ovat nimetty tunnisteen sisällä, ja käytämme kaksoispistettä erottamaan ne toisistaan. Tämä on hyödyllistä, koska nyt molemmat arvot IpAddrKind::V4 ja IpAddrKind::V6 ovat samaa tyyppiä: IpAddrKind. Voimme sitten esimerkiksi määritellä funktion, joka ottaa minkä tahansa IpAddrKind-arvon:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:fn}}
}

Ja voimme kutsua tätä funktiota kummallakin variantilla:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:fn_call}}
}

Enumien käytössä on vielä enemmän etuja. Kun ajattelemme IP-osoitetyyppiämme tarkemmin, tällä hetkellä meillä ei ole tapaa tallentaa varsinaista IP-osoitteen dataa; tiedämme vain sen tyypin. Koska opit juuri rakenteista Luvussa 5, saatat olla taipuvainen ratkaisemaan tämän ongelman rakenteilla, kuten Listauksessa 6-1 on esitetty.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-01/src/main.rs:here}}
}
Listing 6-1: IP-osoitteen datan ja IpAddrKind-variantin tallentaminen struct-rakenteella

Tässä olemme määritelleet IpAddr-rakenteen, jolla on kaksi kenttää: kind-kenttä, jonka tyyppi on IpAddrKind (aiemmin määrittelemämme enum), ja address-kenttä, jonka tyyppi on String. Meillä on kaksi tämän rakenteen instanssia. Ensimmäinen on home, ja sen kind-kentän arvo on IpAddrKind::V4 ja siihen liittyvä osoitedata on 127.0.0.1. Toinen instanssi on loopback. Sen kind-kentän arvo on IpAddrKind-enumin toinen variantti, V6, ja siihen liittyvä osoite on ::1. Olemme käyttäneet rakennetta niputtaaksemme kind- ja address-arvot yhteen, joten nyt variantti on yhdistetty arvoon.

Sama käsite voidaan kuitenkin ilmaista tiiviimmin pelkällä enumilla: sen sijaan, että enum olisi rakenteen sisällä, voimme laittaa datan suoraan kuhunkin enum-varianttiin. Tämä uusi IpAddr-enumin määrittely sanoo, että sekä V4- että V6-variantit sisältävät liittyvät String-arvot:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-02-enum-with-data/src/main.rs:here}}
}

Liitämme datan suoraan kuhunkin enum-varianttiin, joten erillistä rakennetta ei tarvita. Tässä on myös helpompi nähdä toinen yksityiskohta siitä, miten enumit toimivat: jokaisen määrittelemämme enum-variantin nimi muuttuu myös funktioksi, joka rakentaa enum-instanssin. Toisin sanoen IpAddr::V4() on funktiokutsu, joka ottaa String-argumentin ja palauttaa IpAddr-tyypin instanssin. Saamme tämän konstruktorifunktion automaattisesti enumin määrittelyn seurauksena.

Enumin käytössä on toinenkin etu rakenteeseen verrattuna: jokaisella variantilla voi olla eri tyyppejä ja eri määrä liittyvää dataa. Version neljä IP-osoitteissa on aina neljä numeerista komponenttia, joiden arvot ovat välillä 0–255. Jos haluaisimme tallentaa V4-osoitteet neljänä u8-arvona mutta ilmaista V6-osoitteet yhtenä String-arvona, emme voisi tehdä sitä rakenteella. Enumit käsittelevät tämän tapauksen vaivatta:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-03-variants-with-different-data/src/main.rs:here}}
}

Olemme näyttäneet useita eri tapoja määritellä tietorakenteita version neljä ja version kuusi IP-osoitteiden tallentamiseen. Kuitenkin, kuten käy ilmi, IP-osoitteiden tallentaminen ja sen koodaaminen, minkä tyyppisiä ne ovat, on niin yleistä, että standardikirjastossa on määrittely, jota voimme käyttää! Katsotaan, miten standardikirjasto määrittelee IpAddr:n: siinä on täsmälleen sama enum ja variantit, jotka olemme määritelleet ja käyttäneet, mutta se upottaa osoitedatan varianttien sisään kahden eri rakenteen muodossa, jotka on määritelty eri tavoin kullekin variantille:

#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

Tämä koodi havainnollistaa, että enum-variantin sisään voi laittaa minkä tahansa tyyppistä dataa: merkkijonoja, numeerisia tyyppejä tai rakenteita, esimerkiksi. Voit jopa sisällyttää toisen enumin! Lisäksi standardikirjaston tyypit eivät usein ole paljon monimutkaisempia kuin mitä itse keksisit.

Huomaa, että vaikka standardikirjasto sisältää IpAddr-määrittelyn, voimme silti luoda ja käyttää omaa määrittelyämme ilman ristiriitaa, koska emme ole tuoneet standardikirjaston määrittelyä laajuuteemme. Puhumme tyyppejen tuomisesta laajuuteen enemmän Luvussa 7.

Katsotaan toista enum-esimerkkiä Listauksessa 6-2: tässä on laaja kirjo eri tyyppejä upotettuna varianteihin.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-02/src/main.rs:here}}
}
Listing 6-2: Message-enum, jonka variantit tallentavat eri määriä ja tyyppejä arvoja

Tässä enumissa on neljä varianttia eri tyypeillä:

  • Quit ei sisällä lainkaan dataa.
  • Move sisältää nimettyjä kenttiä, kuten rakenne.
  • Write sisältää yhden String-arvon.
  • ChangeColor sisältää kolme i32-arvoa.

Enumin määrittely varianteilla, kuten Listauksessa 6-2, on samankaltaista kuin erilaisten rakennemäärittelyjen määrittely, paitsi että enum ei käytä struct-avainsanaa ja kaikki variantit on ryhmitelty yhteen Message-tyypin alle. Seuraavat rakenteet voisivat tallentaa saman datan, jonka edellä olevat enum-variantit tallentavat:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-04-structs-similar-to-message-enum/src/main.rs:here}}
}

Mutta jos käyttäisimme eri rakenteita, joilla kullakin on oma tyypinsä, emme voisi yhtä helposti määritellä funktiota, joka ottaa minkä tahansa näistä viestityypeistä, kuin voisimme Listauksessa 6-2 määritellyllä Message-enumilla, joka on yksi tyyppi.

Enumeilla ja rakenteilla on vielä yksi yhteinen piirre: aivan kuten voimme määritellä metodeja rakenteille impl:llä, voimme määritellä metodeja myös enumeille. Tässä on call-niminen metodi, jonka voisimme määritellä Message-enumillemme:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-05-methods-on-enums/src/main.rs:here}}
}

Metodin runko käyttäisi self:ää saadakseen arvon, jolle metodia kutsuttiin. Tässä esimerkissä olemme luoneet muuttujan m, jonka arvo on Message::Write(String::from("hello")), ja se on se, mikä self on call-metodin rungossa, kun m.call() suoritetaan.

Katsotaan vielä toista standardikirjaston enumia, joka on hyvin yleinen ja hyödyllinen: Option.

Option-enum ja sen edut verrattuna null-arvoihin

Tämä osio tutkii Option-tapauskatsausta, joka on toinen standardikirjaston määrittelemä enum. Option-tyyppi koodaa hyvin yleisen tilanteen, jossa arvo voi olla jotain tai ei mitään.

Esimerkiksi, jos pyydät ensimmäistä alkiota ei-tyhjästä listasta, saat arvon. Jos pyydät ensimmäistä alkiota tyhjästä listasta, et saa mitään. Tämän käsitteen ilmaiseminen tyyppijärjestelmän kautta tarkoittaa, että kääntäjä voi tarkistaa, oletko käsitellyt kaikki tapaukset, jotka sinun pitäisi käsitellä; tämä ominaisuus voi estää virheitä, jotka ovat erittäin yleisiä muissa ohjelmointikielissä.

Ohjelmointikielen suunnittelua ajatellaan usein sen pohjalta, mitä ominaisuuksia sisällytetään, mutta pois jätetyt ominaisuudet ovat myös tärkeitä. Rustissa ei ole null-ominaisuutta, joka on monissa muissa kielissä. Null on arvo, joka tarkoittaa, ettei siinä ole arvoa. Kielissä, joissa on null, muuttujat voivat aina olla jommassakummassa tilassa: null tai ei-null.

Vuoden 2009 esityksessään ”Null References: The Billion Dollar Mistake” Tony Hoare, nullin keksijä, sanoo seuraavaa:

Kutsun sitä miljardidollarin virheekseni. Silloin suunnittelin ensimmäistä kattavaa tyyppijärjestelmää viittauksille olio-ohjelmointikielessä. Tavoitteenani oli varmistaa, että kaikki viittausten käyttö olisi täysin turvallista ja kääntäjä tarkistaisi sen automaattisesti. Mutta en voinut vastustaa kiusausta laittaa mukaan null-viittaus, yksinkertaisesti koska se oli niin helppo toteuttaa. Tämä on johtanut lukemattomiin virheisiin, haavoittuvuuksiin ja järjestelmän kaatumisiin, jotka ovat todennäköisesti aiheuttaneet miljardin dollarin edestä kipua ja vahinkoa viimeisten neljänkymmenen vuoden aikana.

Ongelma null-arvojen kanssa on, että jos yrität käyttää null-arvoa ei-null-arvona, saat jonkinlaisen virheen. Koska tämä null- tai ei-null-ominaisuus on kaikkialla, on erittäin helppo tehdä tämänkaltaisia virheitä.

Kuitenkin käsite, jota null yrittää ilmaista, on silti hyödyllinen: null on arvo, joka on tällä hetkellä virheellinen tai poissa jostain syystä.

Ongelma ei ole oikeastaan käsitteessä vaan tietyssä toteutuksessa. Siksi Rustissa ei ole null-arvoja, mutta siinä on enum, joka voi koodata käsitteen, että arvo on läsnä tai poissa. Tämä enum on Option<T>, ja se on standardikirjaston määrittelemä seuraavasti:

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Option<T>-enum on niin hyödyllinen, että se on jopa mukana preludessa; sinun ei tarvitse tuoda sitä laajuuteen erikseen. Sen variantit ovat myös mukana preludessa: voit käyttää Some- ja None-variantteja suoraan ilman Option::-etuliitettä. Option<T>-enum on silti tavallinen enum, ja Some(T) ja None ovat edelleen tyyppiä Option<T>.

<T>-syntaksi on Rust-ominaisuus, josta emme ole vielä puhuneet. Se on geneerinen tyyppiparametri, ja käsittelemme geneerisiä tyyppejä tarkemmin Luvussa 10. Toistaiseksi sinun tarvitsee tietää vain, että <T> tarkoittaa, että Option-enumin Some-variantti voi sisältää yhden kappaleen dataa mistä tahansa tyypistä, ja jokainen konkreettinen tyyppi, jota käytetään T:n paikalla, tekee koko Option<T>-tyypistä eri tyypin. Tässä on esimerkkejä Option-arvojen käytöstä numerotyyppejä ja merkkityyppejä tallentaen:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-06-option-examples/src/main.rs:here}}
}

Muuttujan some_number tyyppi on Option<i32>. Muuttujan some_char tyyppi on Option<char>, joka on eri tyyppi. Rust voi päätellä nämä tyypit, koska olemme määrittäneet arvon Some-variantin sisällä. Muuttujan absent_number kohdalla Rust vaatii meitä merkitsemään koko Option-tyypin: kääntäjä ei voi päätellä tyyppiä, jota vastaava Some-variantti sisältäisi, katsomalla pelkkää None-arvoa. Tässä kerromme Rustille, että tarkoitamme absent_number:n olevan tyyppiä Option<i32>.

Kun meillä on Some-arvo, tiedämme, että arvo on läsnä ja se on Some-variantin sisällä. Kun meillä on None-arvo, se tarkoittaa eräässä mielessä samaa kuin null: meillä ei ole kelvollista arvoa. Miksi Option<T> on siis parempi kuin null?

Lyhyesti sanottuna, koska Option<T> ja T (missä T voi olla mikä tahansa tyyppi) ovat eri tyyppejä, kääntäjä ei anna meidän käyttää Option<T>-arvoa ikään kuin se olisi varmasti kelvollinen arvo. Esimerkiksi tämä koodi ei käänny, koska se yrittää lisätä i8:n Option<i8>:aan:

{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-07-cant-use-option-directly/src/main.rs:here}}

Jos ajamme tämän koodin, saamme virheilmoituksen, joka näyttää tältä:

{{#include ../listings/ch06-enums-and-pattern-matching/no-listing-07-cant-use-option-directly/output.txt}}

Voimakasta! Käytännössä tämä virheilmoitus tarkoittaa, että Rust ei ymmärrä, miten i8 ja Option<i8> lisätään yhteen, koska ne ovat eri tyyppejä. Kun meillä on arvo tyypiltään kuten i8 Rustissa, kääntäjä varmistaa, että meillä on aina kelvollinen arvo. Voimme edetä luottavaisin mielin ilman, että meidän täytyy tarkistaa null ennen arvon käyttöä. Vasta kun meillä on Option<i8> (tai mikä tahansa tyyppi, jonka kanssa työskentelemme), meidän täytyy huolehtia siitä, että arvo saattaa puuttua, ja kääntäjä varmistaa, että käsittelemme tämän tapauksen ennen arvon käyttöä.

Toisin sanoen sinun täytyy muuntaa Option<T> arvoksi T, ennen kuin voit suorittaa T:n operaatioita sillä. Yleisesti tämä auttaa havaitsemaan yhden yleisimmistä null-ongelmista: oletus, että jokin ei ole null, vaikka se itse asiassa on.

Virheellisen oletuksen riskin poistaminen auttaa sinua luottamaan koodiisi enemmän. Jotta voisit olla arvo, joka saattaa olla null, sinun täytyy erikseen valita se tekemällä arvon tyypiksi Option<T>. Sitten kun käytät sitä arvoa, sinun täytyy erikseen käsitellä tapaus, jossa arvo on null. Kaikkialla, missä arvolla on tyyppi, joka ei ole Option<T>, voit turvallisesti olettaa, ettei arvo ole null. Tämä oli tarkoituksellinen suunnittelupäätös Rustissa rajoittaakseen nullin yleisyyttä ja lisätäkseen Rust-koodin turvallisuutta.

Miten siis saat T-arvon Some-variantista, kun sinulla on Option<T>-tyyppinen arvo, jotta voit käyttää sitä arvoa? Option<T>-enumilla on suuri määrä metodeja, jotka ovat hyödyllisiä eri tilanteissa; voit tutustua niihin sen dokumentaatiossa. Option<T>-metodien tunteminen on erittäin hyödyllistä Rust-matkallasi.

Yleisesti ottaen, jotta voit käyttää Option<T>-arvoa, haluat koodia, joka käsittelee kunkin variantin. Haluat koodia, joka suoritetaan vain, kun sinulla on Some(T)-arvo, ja tämä koodi saa käyttää sisäistä T:tä. Haluat toisenlaista koodia, joka suoritetaan vain, jos sinulla on None-arvo, eikä tässä koodissa ole T-arvoa käytettävissä. match-lauseke on ohjausrakenne, joka tekee juuri tämän enumien kanssa käytettynä: se suorittaa eri koodia riippuen siitä, mikä enumin variantti sillä on, ja tämä koodi voi käyttää vastaavan arvon sisällä olevaa dataa.

match-ohjausrakenteen konstruktio

match-ohjausrakenne

Rustissa on erittäin tehokas ohjausrakenne nimeltä match, jonka avulla voit verrata arvoa sarjaan kuvioita ja suorittaa sitten koodia sen perusteella, mikä kuvio täsmää. Kuviot voivat koostua kirjaimellisista arvoista, muuttujien nimistä, jokerimerkeistä ja monista muista asioista; Luku 19 käsittelee kaikki erilaiset kuviotyypit ja niiden toiminnan. match-lausekkeen voima tulee kuvioiden ilmaisukyvystä ja siitä, että kääntäjä varmistaa kaikkien mahdollisten tapausten käsitellyn.

Voit ajatella match-lauseketta kolikonlajittelukoneena: kolikot liukuvat radalla, jossa on erikokoisia reikiä, ja jokainen kolikko putoaa ensimmäiseen reikään, johon se mahtuu. Samalla tavalla arvot käyvät läpi match-lausekkeen jokaisen kuvion, ja ensimmäiseen kuvioon, johon arvo ”mahtuu”, arvo putoaa liittyvään koodilohkoon suoritusta varten.

Puhutaanpa kolikoista — käytetään niitä esimerkkinä match-lausekkeen kanssa! Voimme kirjoittaa funktion, joka ottaa tuntemattoman Yhdysvaltain kolikon ja määrittää samankaltaisella tavalla kuin laskuri, mikä kolikko se on, ja palauttaa sen arvon senteissä, kuten Listauksessa 6-3 on esitetty.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-03/src/main.rs:here}}
}
Listing 6-3: Enum ja match-lauseke, jonka kuviot ovat enumin variantteja

Puretaan value_in_cents-funktion match-lauseke. Ensin listaamme match-avainsanan ja sitten lausekkeen, joka tässä tapauksessa on arvo coin. Tämä näyttää hyvin samalta kuin ehtolauseke if:n kanssa, mutta ero on suuri: if:ssä ehdon täytyy evaluoitua totuusarvoksi, mutta tässä se voi olla mikä tahansa tyyppi. Tässä esimerkissä coin-muuttujan tyyppi on ensimmäisellä rivillä määritelty Coin-enum.

Seuraavaksi tulevat match-haarat. Haaralla on kaksi osaa: kuvio ja jokin koodi. Ensimmäisellä haaralla on kuvio, joka on arvo Coin::Penny, ja sitten =>-operaattori, joka erottaa kuvion ja suoritettavan koodin. Tässä tapauksessa koodi on vain arvo 1. Jokainen haara erotetaan seuraavasta pilkulla.

Kun match-lauseke suoritetaan, se vertaa tulosarvoa kunkin haaran kuvioon järjestyksessä. Jos kuvio täsmää arvoon, kyseiseen kuvioon liittyvä koodi suoritetaan. Jos kuvio ei täsmää arvoon, suoritus jatkuu seuraavaan haaraan, aivan kuten kolikonlajittelukoneessa. Meillä voi olla niin monta haaraa kuin tarvitsemme: Listauksessa 6-3 match-lausekkeessamme on neljä haaraa.

Kuhunkin haaraan liittyvä koodi on lauseke, ja vastaavan haaran lausekkeen tulosarvo on koko match-lausekkeen palauttama arvo.

Emme yleensä käytä aaltosulkeita, jos haaran koodi on lyhyt, kuten Listauksessa 6-3, jossa jokainen haara palauttaa vain arvon. Jos haluat suorittaa useita koodirivejä haarassa, sinun täytyy käyttää aaltosulkeita, ja haaraa seuraava pilkku on silloin valinnainen. Esimerkiksi seuraava koodi tulostaa ”Lucky penny!” aina, kun metodia kutsutaan Coin::Penny-arvolla, mutta palauttaa silti lohkon viimeisen arvon, 1:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-08-match-arm-multiple-lines/src/main.rs:here}}
}

Arvoihin sitoutuvat kuviot

Toinen hyödyllinen ominaisuus match-haaroissa on, että ne voivat sitoutua kuvioon täsmäävien arvojen osiin. Näin voimme poimia arvoja enum-varianttien sisältä.

Esimerkkinä muutetaan yksi enum-varianteistamme pitämään dataa sisällään. Vuosina 1999–2008 Yhdysvaltain rahapaja lyö 25 sentin kolikoita, joissa oli eri suunnitelma jokaiselle 50 osavaltiolle toisella puolella. Mikään muu kolikko ei saanut osavaltiosuunnitelmia, joten vain 25 sentin kolikoilla on tämä ylimääräinen arvo. Voimme lisätä tämän tiedon enum-määrittelyymme muuttamalla Quarter-variantin sisältämään UsState-arvon, kuten Listauksessa 6-4 on tehty.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-04/src/main.rs:here}}
}
Listing 6-4: Coin-enum, jossa Quarter-variantti sisältää myös UsState-arvon

Kuvitellaan, että ystävä yrittää kerätä kaikki 50 osavaltion 25 sentin kolikkoa. Kun lajittelemme irtonaisia kolikoita tyypin mukaan, mainitsemme myös jokaisen 25 sentin kolikon osavaltion nimen, jotta ystävä voi lisätä sen kokoelmaansa, jos sitä ei vielä ole.

Tämän koodin match-lausekkeessa lisäämme muuttujan nimeltä state kuvioon, joka täsmää Coin::Quarter-variantin arvoihin. Kun Coin::Quarter täsmää, state-muuttuja sitoutuu kyseisen 25 sentin kolikon osavaltion arvoon. Sitten voimme käyttää state:a kyseisen haaran koodissa näin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-09-variable-in-pattern/src/main.rs:here}}
}

Jos kutsuisimme value_in_cents(Coin::Quarter(UsState::Alaska)), coin olisi Coin::Quarter(UsState::Alaska). Kun vertaamme tätä arvoa jokaiseen match-haaraan, mikään ei täsmää ennen kuin pääsemme Coin::Quarter(state)-haaraan. Tällöin state-sidonta on arvo UsState::Alaska. Voimme sitten käyttää tätä sidontaa println!-lausekkeessa ja poimia näin sisäisen osavaltioarvon Coin-enumin Quarter-variantista.

Option<T>-kuvio match-lausekkeessa

Edellisessä osiossa halusimme saada sisäisen T-arvon Some-tapauksesta käytettäessä Option<T>:tä; voimme käsitellä Option<T>:tä myös match-lausekkeella, kuten teimme Coin-enumin kanssa! Sen sijaan, että vertaisimme kolikoita, vertaamme Option<T>:n variantteja, mutta match-lausekkeen toimintatapa pysyy samana.

Oletetaan, että haluamme kirjoittaa funktion, joka ottaa Option<i32>:n ja lisää 1 sisällä olevaan arvoon, jos arvo on olemassa. Jos arvoa ei ole, funktion pitäisi palauttaa None-arvo eikä yrittää suorittaa mitään operaatioita.

Tämä funktio on hyvin helppo kirjoittaa match-lausekkeen ansiosta, ja se näyttää Listaukselta 6-5.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:here}}
}
Listing 6-5: Funktio, joka käyttää match-lauseketta Option<i32>:llä

Tarkastellaan plus_one-funktion ensimmäistä suoritusta tarkemmin. Kun kutsumme plus_one(five), muuttujalla x plus_one-funktion rungossa on arvo Some(5). Vertaamme sitten sitä jokaiseen match-haaraan:

{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:first_arm}}

Some(5)-arvo ei täsmää kuvioon None, joten jatkamme seuraavaan haaraan:

{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:second_arm}}

Täsmääkö Some(5) kuvioon Some(i)? Kyllä! Meillä on sama variantti. i sitoutuu Some:n sisältämään arvoon, joten i saa arvon 5. Haaran koodi suoritetaan sitten, joten lisäämme 1 arvoon i ja luomme uuden Some-arvon, jonka sisällä on yhteensä 6.

Tarkastellaan nyt Listauksen 6-5 toista plus_one-kutsua, jossa x on None. Siirrymme match-lausekkeeseen ja vertaamme ensimmäiseen haaraan:

{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:first_arm}}

Se täsmää! Lisättävää arvoa ei ole, joten ohjelma pysähtyy ja palauttaa None-arvon =>-merkin oikealla puolella. Koska ensimmäinen haara täsmäsi, muita haaroja ei verrata.

match-lausekkeen ja enumien yhdistäminen on hyödyllistä monissa tilanteissa. Näet tämän kuvion paljon Rust-koodissa: match enumia vasten, sitoa muuttuja sisällä olevaan dataan ja suorita sitten koodia sen perusteella. Se on aluksi hieman hankalaa, mutta kun tottuu siihen, toivot sitä kaikissa kielissä. Se on jatkuvasti käyttäjien suosikki.

match-lausekkeet ovat tyhjentäviä

match-lausekkeesta on vielä yksi puoli, josta meidän täytyy puhua: haarojen kuvioiden täytyy kattaa kaikki mahdollisuudet. Harkitse tätä versiota plus_one-funktiostamme, jossa on bugi eikä se käänny:

{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-10-non-exhaustive-match/src/main.rs:here}}

Emme käsitelleet None-tapausta, joten tämä koodi aiheuttaa bugin. Onneksi Rust osaa havaita tämän bugin. Jos yritämme kääntää tämän koodin, saamme tämän virheen:

{{#include ../listings/ch06-enums-and-pattern-matching/no-listing-10-non-exhaustive-match/output.txt}}

Rust tietää, ettemme käsitelleet kaikkia mahdollisia tapauksia, ja tietää jopa, minkä kuvion unohdimme! match-lausekkeet Rustissa ovat tyhjentäviä: meidän täytyy käsitellä jokainen mahdollisuus, jotta koodi on kelvollinen. Erityisesti Option<T>:n tapauksessa, kun Rust estää meitä unohtamasta None-tapauksen käsittelyn eksplisiittisesti, se suojaa meitä olettamasta, että meillä on arvo, vaikka se saattaa olla null, mikä tekee aiemmin käsitellystä miljardin dollarin virheestä mahdottoman.

Yleiskuviot ja _-paikkamerkki

Enumien avulla voimme myös tehdä erityistoimia muutamalle tietylle arvolle, mutta kaikille muille arvoille tehdä yhden oletustoiminnon. Kuvitellaan, että toteutamme pelin, jossa jos heität noppaa ja saat 3, pelaajasi ei liiku vaan saa uuden hienon hatun. Jos heität 7, pelaajasi menettää hienon hatun. Kaikille muille arvoille pelaajasi liikkuu pelilaudalla sen verran tilaa kuin noppa osoittaa. Tässä on match, joka toteuttaa tämän logiikan nopan heittotuloksella kovakoodattuna satunnaisen arvon sijaan, ja kaikki muu logiikka edustettu funktioina ilman runkoa, koska niiden toteuttaminen ei kuulu tämän esimerkin piiriin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-15-binding-catchall/src/main.rs:here}}
}

Ensimmäisillä kahdella haaralla kuviot ovat kirjaimellisia arvoja 3 ja 7. Viimeisellä haaralla, joka kattaa kaikki muut mahdolliset arvot, kuvio on muuttuja, jonka olemme valinneet nimeksi other. other-haaran koodi käyttää muuttujaa välittämällä sen move_player-funktiolle.

Tämä koodi kääntyy, vaikka emme listanneet kaikkia mahdollisia u8-arvoja, koska viimeinen kuvio täsmää kaikkiin arvoihin, joita ei ole erikseen listattu. Tämä yleiskuvio täyttää vaatimuksen, että match on tyhjentävä. Huomaa, että yleiskuvion täytyy olla viimeisenä, koska kuvioita evaluoidaan järjestyksessä. Jos yleiskuvio olisi aiemmin, muut haarat eivät koskaan suorittuisi, joten Rust varoittaa meitä, jos lisäämme haaroja yleiskuvion jälkeen!

Rustissa on myös kuvio, jota voimme käyttää, kun haluamme yleiskuvion mutta emme halua käyttää yleiskuvion arvoa: _ on erityinen kuvio, joka täsmää mihin tahansa arvoon eikä sido sitä arvoon. Tämä kertoo Rustille, ettei meidän tarvitse arvoa, joten Rust ei varoita meitä käyttämättömästä muuttujasta.

Muutetaan pelin sääntöjä: nyt, jos heität mitä tahansa muuta kuin 3 tai 7, sinun täytyy heittää uudelleen. Emme enää tarvitse yleiskuvion arvoa, joten voimme muuttaa koodimme käyttämään _-merkkiä other-nimisen muuttujan sijaan:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-16-underscore-catchall/src/main.rs:here}}
}

Tämä esimerkki täyttää myös tyhjentävyysvaatimuksen, koska sivuutamme eksplisiittisesti kaikki muut arvot viimeisessä haarassa; emme ole unohtaneet mitään.

Lopuksi muutamme pelin sääntöjä vielä kerran niin, ettei vuorollasi tapahdu mitään, jos heität mitä tahansa muuta kuin 3 tai 7. Voimme ilmaista tämän käyttämällä yksikköarvoa (tyhjä tuplatyyppi, josta mainittiin ”Tuplatyyppi” -osiossa) koodina, joka liittyy _-haaraan:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-17-underscore-unit/src/main.rs:here}}
}

Tässä kerromme Rustille eksplisiittisesti, ettei meidän tarvitse käyttää mitään muuta arvoa, joka ei täsmää aiemman haaran kuvioon, emmekä halua suorittaa mitään koodia tässä tapauksessa.

Kuvioista ja täsmäyksestä on lisää, mitä käsittelemme Luvussa 19. Toistaiseksi siirrymme if let-syntaksiin, joka voi olla hyödyllinen tilanteissa, joissa match-lauseke on hieman puhelias.

Ytimekäs ohjausrakenne if let- ja let else-lauseilla

Tiivis ohjausrakenne if let- ja let...else-rakenteilla

if let-syntaksi antaa yhdistää if- ja let-rakenteet vähemmän puhelijaan tapaan käsitellä arvoja, jotka täsmäävät yhteen kuvioon, ja sivuuttaa loput. Harkitse Listauksen 6-6 ohjelmaa, joka täsmää Option<u8>-arvon config_max-muuttujassa, mutta haluaa suorittaa koodia vain, jos arvo on Some-variantti.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-06/src/main.rs:here}}
}
Listing 6-6: match, joka välittää vain Some-arvon suorittamisesta koodia

Jos arvo on Some, tulostamme Some-variantin arvon sitomalla arvon muuttujaan max kuviossa. Emme halua tehdä mitään None-arvolla. match-lausekkeen tyydyttämiseksi meidän täytyy lisätä _ => () yhden variantin käsittelyn jälkeen, mikä on ärsyttävää ylimääräistä koodia.

Sen sijaan voisimme kirjoittaa tämän lyhyemmällä tavalla käyttämällä if let-rakennetta. Seuraava koodi käyttäytyy samoin kuin Listauksen 6-6 match:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-12-if-let/src/main.rs:here}}
}

if let-syntaksi ottaa kuvion ja lausekkeen, jotka erotetaan yhtäsuuruusmerkillä. Se toimii samalla tavalla kuin match, jossa lauseke annetaan match:lle ja kuvio on sen ensimmäinen haara. Tässä tapauksessa kuvio on Some(max), ja max sitoutuu Some:n sisällä olevaan arvoon. Voimme sitten käyttää max:ia if let-lohkon rungossa samalla tavalla kuin käytimme max:ia vastaavassa match-haarassa. if let-lohkon koodi suoritetaan vain, jos arvo täsmää kuvioon.

if let tarkoittaa vähemmän kirjoittamista, vähemmän sisennystä ja vähemmän ylimääräistä koodia. Menetät kuitenkin match-lausekkeen pakottaman tyhjentävän tarkistuksen, joka varmistaa, ettei sinun unohda käsitellä mitään tapauksia. Valinta match- ja if let-rakenteiden välillä riippuu siitä, mitä teet tietyssä tilanteessa, ja siitä, onko tiiviys sopiva kompromissi tyhjentävän tarkistuksen menettämisestä.

Toisin sanoen voit ajatella if let:iä syntaksisokerina match-lausekkeelle, joka suorittaa koodia, kun arvo täsmää yhteen kuvioon, ja sitten sivuuttaa kaikki muut arvot.

Voimme liittää else-haaran if let-rakenteeseen. else-haaraan liittyvä koodilohko on sama kuin koodilohko, joka menisi _-tapaukseen match-lausekkeessa, joka vastaa if let- ja else-rakennetta. Muista Listauksen 6-4 Coin-enumin määrittely, jossa Quarter-variantti sisälsi myös UsState-arvon. Jos haluaisimme laskea kaikki ei-25-senttiset kolikot ja samalla ilmoittaa 25 sentin kolikoiden osavaltiot, voisimme tehdä sen match-lausekkeella näin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-13-count-and-announce-match/src/main.rs:here}}
}

Tai voisimme käyttää if let- ja else-lauseketta näin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-14-count-and-announce-if-let-else/src/main.rs:here}}
}

Pysyminen ”onnellisella polulla” let...else-rakenteella

Yleinen kuvio on suorittaa laskenta, kun arvo on läsnä, ja palauttaa oletusarvo muuten. Jatkamalla kolikkoesimerkkiämme UsState-arvolla, jos haluaisimme sanoa jotain hauskaa riippuen siitä, kuinka vanha 25 sentin kolikon osavaltio on, voisimme lisätä UsState-rakenteeseen metodin, joka tarkistaa osavaltion iän, näin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-07/src/main.rs:state}}
}

Sitten voisimme käyttää if let:iä täsmätäksemme kolikon tyyppiin ja esitelläksemme state-muuttujan ehdon rungon sisällä, kuten Listauksessa 6-7.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-07/src/main.rs:describe}}
}
Listing 6-7: Tarkistus, oliko osavaltio olemassa vuonna 1900, käyttämällä ehtorakenteita if let:n sisällä

Se hoitaa asian, mutta on siirtänyt työn if let-lausekkeen runkoon, ja jos tehtävä on monimutkaisempi, voi olla vaikea seurata, miten ylätason haarat liittyvät toisiinsa. Voisimme myös hyödyntää sitä, että lausekkeet tuottavat arvon, joko tuottaaksemme state:n if let:stä tai palauttaaksemme aikaisin, kuten Listauksessa 6-8. (Voit tehdä jotain vastaavaa myös match-lausekkeella.)

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-08/src/main.rs:describe}}
}
Listing 6-8: if let:n käyttö arvon tuottamiseen tai aikaisen paluun tekemiseen

Tämä on kuitenkin omalla tavallaan hieman ärsyttävää seurata! Yksi if let-haara tuottaa arvon, ja toinen palaa kokonaan funktiosta.

Tämän yleisen kuvion ilmaisemiseksi miellyttävämmällä tavalla Rustissa on let...else. let...else-syntaksi ottaa kuvion vasemmalle puolelle ja lausekkeen oikealle puolelle, hyvin samankaltaisesti kuin if let, mutta siinä ei ole if-haaraa, vain else-haara. Jos kuvio täsmää, se sitoo arvon kuviosta ulompaan näkyvyysalueeseen. Jos kuvio ei täsmää, ohjelma siirtyy else-haaraan, jonka täytyy palata funktiosta.

Listauksessa 6-9 näet, miltä Listaus 6-8 näyttää, kun if let korvataan let...else-rakenteella.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-09/src/main.rs:describe}}
}
Listing 6-9: let...else:n käyttö funktion kulun selkeyttämiseen

Huomaa, että näin pysytään ”onnellisella polulla” funktion päärungossa ilman merkittävästi erilaista ohjausrakennetta kahdelle haaralle, kuten if let teki.

Jos ohjelmassasi on logiikkaa, joka on liian puheliasta ilmaistavaksi match-lausekkeella, muista, että if let ja let...else ovat myös Rust-työkalupakissasi.

Yhteenveto

Olemme nyt käsitelleet, miten enumien avulla luodaan mukautettuja tyyppejä, jotka voivat olla yksi joukosta lueteltuja arvoja. Olemme näyttäneet, miten standardikirjaston Option<T>-tyyppi auttaa käyttämään tyyppijärjestelmää virheiden estämiseen. Kun enum-arvoilla on dataa sisällään, voit käyttää match- tai if let-rakennetta poimiaksesi ja käyttääksesi näitä arvoja riippuen siitä, kuinka monta tapausta sinun täytyy käsitellä.

Rust-ohjelmasi voivat nyt ilmaista sovellusalueesi käsitteitä rakenteiden ja enumien avulla. Mukautettujen tyyppien luominen API:isi käyttöön varmistaa tyyppiturvallisuuden: kääntäjä varmistaa, että funktiosi saavat vain sellaisia arvoja, joita kukin funktio odottaa.

Jotta voit tarjota käyttäjillesi hyvin organisoidun ja suoraviivaisen API:n, joka paljastaa juuri sen, mitä käyttäjät tarvitsevat, siirrytään nyt Rustin moduuleihin.

Suurten projektien hallinta pakettien, laatikoiden ja moduulien avulla

Kun ohjelmasi kasvaa, sen rakenteen järjestäminen tulee yhä tärkeämmäksi. Ryhmittelemällä toisiinsa liittyvät toiminnallisuudet ja erottamalla erilliset ominaisuudet koodista voit helpottaa koodin hallintaa. Tämä tekee selväksi, mistä tietty ominaisuus löytyy ja missä sitä voi muokata.

Tähän asti kirjoittamamme ohjelmat ovat olleet yhdessä moduulissa ja yhdessä tiedostossa. Kun projekti kasvaa, koodi kannattaa jakaa useisiin moduuleihin ja tiedostoihin. Paketti voi sisältää useita binäärisiä laatikoita (crates) sekä valinnaisesti yhden kirjastolaatikon. Kun paketti kasvaa, voit erottaa osia omiksi laatikoiksi, jolloin ne voivat toimia ulkoisina riippuvuuksina. Tämä luku kattaa nämä tekniikat. Suurille projekteille, joissa on useita toisiinsa liittyviä paketteja, Cargo tarjoaa työtilat (workspaces), joita käsittelemme luvussa 14.

Käsittelemme myös yksityiskohtien kapselointia, joka mahdollistaa koodin uudelleenkäytön korkeammalla tasolla. Kun olet toteuttanut jonkin toiminnon, muu koodi voi kutsua sitä sen julkisen rajapinnan kautta ilman, että tarvitsee tietää toteutuksen yksityiskohtia. Tämä rajaa sitä tietomäärää, joka sinun täytyy pitää mielessäsi.

Aiheeseen liittyy myös näkyvyysalue (scope): koodin kirjoittamisen konteksti määrää, mitkä nimet ovat käytettävissä. Kun ohjelmoija tai kääntäjä käsittelee koodia, sen on tiedettävä, viittaako jokin nimi muuttujaan, funktioon, rakenteeseen, enum-tyyppiin, moduuliin, vakioon tai muuhun. Voit määritellä näkyvyysalueita ja päättää, mitkä nimet ovat käytettävissä missäkin kohdassa. Et voi antaa kahta saman nimistä elementtiä samassa näkyvyysalueessa, mutta Rust tarjoaa työkaluja nimikonfliktien ratkaisemiseen.

Rust tarjoaa useita ominaisuuksia, joilla voit hallita koodisi rakennetta, mukaan lukien sen, mitkä yksityiskohdat ovat julkisia, mitkä yksityisiä ja mitkä nimet ovat käytettävissä missäkin kontekstissa. Näitä ominaisuuksia kutsutaan usein moduulijärjestelmäksi, ja ne sisältävät:

  • Paketit (Packages): Cargo-ominaisuus, joka mahdollistaa laatikoiden rakentamisen, testaamisen ja jakamisen.
  • Laatikot (Crates): Moduulipuu, joka tuottaa kirjaston tai suoritettavan tiedoston.
  • Moduulit ja use: Ohjaavat koodin rakennetta, näkyvyyttä ja polkujen käyttöä.
  • Polut (Paths): Määrittävät, miten nimetään elementtejä, kuten rakenteita, funktioita tai moduuleja.

Tässä luvussa käsittelemme näitä ominaisuuksia, niiden yhteistoimintaa ja sitä, miten niitä käytetään näkyvyysalueiden hallintaan. Tavoitteena on, että tämän luvun jälkeen ymmärrät Rustin moduulijärjestelmän ja pystyt hallitsemaan näkyvyysalueita sujuvasti!

Paketit ja cratet

Paketit ja crate:t

Moduulijärjestelmän ensimmäiset osat, joita käsittelemme, ovat paketit ja crate:t.

Crate on pienin määrä koodia, jota Rust-kääntäjä käsittelee kerrallaan. Vaikka käyttäisit rustc-komentoa cargo-komennon sijaan ja antaisit yhden lähdekooditiedoston (kuten teimme jo ”Rust-ohjelman perusteet”-kohdassa luvussa 1), kääntäjä pitää kyseistä tiedostoa crate:na. Crate:t voivat sisältää moduuleja, ja moduulit voidaan määritellä muissa tiedostoissa, jotka käännetään crate:n mukana, kuten tulemme näkemään seuraavissa osioissa.

Crate voi olla jommassakummassa muodossa: binääricrate tai kirjastocrate. Binääricrate:t ovat ohjelmia, jotka voidaan kääntää ajettavaksi suoritettavaksi tiedostoksi, kuten komentoriviohjelma tai palvelin. Jokaisella on oltava main-funktio, joka määrittää, mitä tapahtuu suoritettavan käynnistyessä. Kaikki tähän mennessä luomamme crate:t ovat olleet binääricrate:ja.

Kirjastocrate:illa ei ole main-funktiota, eivätkä ne käänny suoritettaviksi tiedostoiksi. Sen sijaan ne määrittelevät toiminnallisuutta, joka on tarkoitettu jaettavaksi useiden projektien kesken. Esimerkiksi rand-crate, jota käytimme luvussa 2, tarjoaa toiminnallisuutta satunnaisten lukujen generointiin. Useimmiten kun rustilaiset sanovat ”crate”, he tarkoittavat kirjastocrate:a, ja he käyttävät sanaa ”crate” vaihdettavasti yleisen ohjelmointikäsitteen ”kirjasto” kanssa.

Crate-juuri on lähdekooditiedosto, josta Rust-kääntäjä aloittaa ja joka muodostaa crate:si juurimoduulin (käsittelemme moduuleja perusteellisesti kohdassa ”Laajuuden ja yksityisyyden hallinta moduuleilla”).

Paketti on yhden tai useamman crate:n kokoelma, joka tarjoaa tietyn toiminnallisuuden. Paketti sisältää Cargo.toml-tiedoston, joka kuvaa, miten nämä crate:t rakennetaan. Cargo on itse asiassa paketti, joka sisältää binääricrate:n komentorivityökalulle, jota olet käyttänyt koodisi kääntämiseen. Cargo-paketti sisältää myös kirjastocrate:n, josta binääricrate riippuu. Muut projektit voivat riippua Cargo-kirjastocrate:sta käyttääkseen samaa logiikkaa, jota Cargo-komentorivityökalu käyttää.

Paketti voi sisältää niin monta binääricrate:a kuin haluat, mutta enintään yhden kirjastocrate:n. Paketissa on oltava vähintään yksi crate, olipa se kirjasto- tai binääricrate.

Käydään läpi, mitä tapahtuu, kun luomme paketin. Ensin annamme komennon cargo new my-project:

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

Kun olemme suorittaneet cargo new my-project, käytämme ls-komentoa nähdäksemme, mitä Cargo luo. my-project-hakemistossa on Cargo.toml-tiedosto, joka muodostaa paketin. Siellä on myös src-hakemisto, joka sisältää main.rs-tiedoston. Avaa Cargo.toml tekstieditorissasi ja huomaa, ettei siinä mainita src/main.rs:ää. Cargo noudattaa käytäntöä, jonka mukaan src/main.rs on binääricrate:n crate-juuri, jolla on sama nimi kuin paketilla. Vastaavasti Cargo tietää, että jos pakettihakemisto sisältää src/lib.rs:n, paketti sisältää kirjastocrate:n, jolla on sama nimi kuin paketilla, ja src/lib.rs on sen crate-juuri. Cargo välittää crate-juuritiedostot rustc-kääntäjälle kirjaston tai binäärin rakentamiseksi.

Tässä meillä on paketti, joka sisältää vain src/main.rs:n, eli se sisältää vain binääricrate:n nimeltä my-project. Jos paketti sisältää sekä src/main.rs:n että src/lib.rs:n, siinä on kaksi crate:a: binääri ja kirjasto, molemmilla sama nimi kuin paketilla. Paketissa voi olla useita binääricrate:ja sijoittamalla tiedostoja src/bin-hakemistoon: jokainen tiedosto on erillinen binääricrate.

Moduulien määrittäminen laajuuden ja yksityisyyden hallitsemiseksi

Laajuuden ja yksityisyyden hallinta moduuleilla

Tässä osiossa käsittelemme moduuleja ja moduulijärjestelmän muita osia, nimittäin polkuja, joiden avulla voit nimetä kohteita; use-avainsanaa, joka tuo polun laajuuteen; ja pub-avainsanaa, jolla kohteet tehdään julkisiksi. Käsittelemme myös as-avainsanaa, ulkoisia paketteja ja glob-operaattoria.

Moduulien pikaopas

Ennen kuin siirrymme moduulien ja polkujen yksityiskohtiin, tässä on nopea viite siitä, miten moduulit, polut, use-avainsana ja pub-avainsana toimivat kääntäjässä ja miten useimmat kehittäjät järjestävät koodinsa. Käymme läpi esimerkkejä jokaisesta näistä säännöistä koko luvun ajan, mutta tämä on hyvä paikka muistuttaa itseään moduulien toiminnasta.

  • Aloita crate-juuresta: Kun crate käännetään, kääntäjä etsii ensin crate-juuritiedostosta (yleensä src/lib.rs kirjastocrate:lle ja src/main.rs binääricrate:lle) käännettävää koodia.
  • Moduulien määrittely: Crate-juuritiedostossa voit määritellä uusia moduuleja; oletetaan, että määrittelet ”garden”-moduulin komennolla mod garden;. Kääntäjä etsii moduulin koodia näistä paikoista:
    • Inline, aaltosulkeissa, jotka korvaavat mod garden -lauseen lopussa olevan puolipisteen
    • Tiedostosta src/garden.rs
    • Tiedostosta src/garden/mod.rs
  • Alimoduulien määrittely: Missä tahansa tiedostossa paitsi crate-juuressa voit määritellä alimoduuuleja. Esimerkiksi voit määritellä mod vegetables; tiedostossa src/garden.rs. Kääntäjä etsii alimoduuulin koodia emomoduulin nimisestä hakemistosta näistä paikoista:
    • Inline, suoraan mod vegetables -lauseen jälkeen aaltosulkeissa puolipisteen sijaan
    • Tiedostosta src/garden/vegetables.rs
    • Tiedostosta src/garden/vegetables/mod.rs
  • Polut moduulien koodiin: Kun moduuli on osa crate:asi, voit viitata kyseisen moduulin koodiin mistä tahansa muualta samassa crate:ssa, kunhan yksityisyyssäännöt sen sallivat, käyttämällä polkua koodiin. Esimerkiksi Asparagus-tyyppi garden-moduulin vegetables-alimoduuulissa löytyy polusta crate::garden::vegetables::Asparagus.
  • Yksityinen vs. julkinen: Moduulin sisällä oleva koodi on oletuksena yksityistä emomoduuleilleen. Tehdäksesi moduulin julkiseksi, määrittele se komennolla pub mod mod-komennon sijaan. Tehdäksesi julkisen moduulin kohteet myös julkisiksi, käytä pub-avainsanaa niiden määrittelyjen edessä.
  • use-avainsana: Laajuudessa use-avainsana luo oikoteitä kohteille vähentääkseen pitkien polkujen toistoa. Missä tahansa laajuudessa, jossa voidaan viitata crate::garden::vegetables::Asparagus-polkuun, voit luoda oikotien komennolla use crate::garden::vegetables::Asparagus;, ja sen jälkeen sinun tarvitsee vain kirjoittaa Asparagus käyttääksesi kyseistä tyyppiä laajuudessa.

Tässä luomme binääricrate:n nimeltä backyard, joka havainnollistaa näitä sääntöjä. Crate:n hakemisto, joka myös on nimeltään backyard, sisältää nämä tiedostot ja hakemistot:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

Tässä tapauksessa crate-juuritiedosto on src/main.rs, ja se sisältää:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/quick-reference-example/src/main.rs}}

pub mod garden; -rivi kertoo kääntäjälle sisällyttää koodi, jonka se löytää tiedostosta src/garden.rs, joka on:

Filename: src/garden.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/quick-reference-example/src/garden.rs}}

Tässä pub mod vegetables; tarkoittaa, että koodi tiedostosta src/garden/vegetables.rs sisällytetään myös. Kyseinen koodi on:

{{#rustdoc_include ../listings/ch07-managing-growing-projects/quick-reference-example/src/garden/vegetables.rs}}

Siirrytään nyt näiden sääntöjen yksityiskohtiin ja havainnollistetaan niitä käytännössä!

Liittyvän koodin ryhmittely moduuleihin

Moduulit antavat meille mahdollisuuden järjestää koodia crate:n sisällä luettavuuden ja helpon uudelleenkäytön vuoksi. Moduulit antavat myös hallita kohteiden yksityisyyttä, koska moduulin sisällä oleva koodi on oletuksena yksityistä. Yksityiset kohteet ovat sisäisiä toteutustietoja, jotka eivät ole ulkopuolisen käytön saatavilla. Voimme valita tehdä moduulit ja niiden sisällä olevat kohteet julkisiksi, mikä paljastaa ne ulkoisen koodin käytettäväksi ja riippuvuudeksi.

Esimerkkinä kirjoitetaan kirjastocrate, joka tarjoaa ravintolan toiminnallisuuden. Määrittelemme funktioiden signatuurit, mutta jätämme niiden rungot tyhjiksi keskittyäksemme koodin organisointiin ravintolan toteutuksen sijaan.

Ravintola-alalla ravintolan osia kutsutaan etu- ja takaosaksi. Etuosassa asiakkaat ovat; se kattaa paikat, joissa emännät istuttavat asiakkaat, tarjoilijat ottavat tilaukset ja maksut, ja baarimestarit valmistavat juomia. Takaosassa keitit ja kokit työskentelevät keittiössä, tiskinpesijät siivoavat, ja esimiehet tekevät hallinnollista työtä.

Rakentaaksemme crate:amme tällä tavalla voimme järjestää sen funktiot sisäkkäisiin moduuleihin. Luo uusi kirjasto nimeltä restaurant suorittamalla cargo new restaurant --lib. Syötä sitten listauksen 7-1 koodi tiedostoon src/lib.rs määritelläksesi moduuleja ja funktiosignatuureja; tämä koodi on etuosan osio.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-01/src/lib.rs}}
Listing 7-1: front_of_house-moduuli, joka sisältää muita moduuleja, jotka sisältävät funktioita

Määrittelemme moduulin mod-avainsanalla, jota seuraa moduulin nimi (tässä tapauksessa front_of_house). Moduulin runko menee sitten aaltosulkeiden sisään. Moduulien sisällä voimme sijoittaa muita moduuleja, kuten tässä tapauksessa moduulit hosting ja serving. Moduulit voivat myös sisältää määrittelyjä muille kohteille, kuten structeille, enumeille, vakioille, trait:eille ja kuten listauksessa 7-1 funktioille.

Moduuleja käyttämällä voimme ryhmitellä liittyvät määrittelyt yhteen ja nimetä, miksi ne liittyvät toisiinsa. Tätä koodia käyttävät ohjelmoijat voivat navigoida koodissa ryhmien perusteella sen sijaan, että heidän pitäisi lukea läpi kaikki määrittelyt, mikä helpottaa heille relevanttien määrittelyjen löytämistä. Tähän koodiin uutta toiminnallisuutta lisäävät ohjelmoijat tietävät, mihin sijoittaa koodin pitääkseen ohjelman järjestyksessä.

Aiemmin mainitsimme, että src/main.rs ja src/lib.rs kutsutaan crate-juuriksi. Niiden nimen syy on se, että kummankin näistä kahdesta tiedostosta sisältö muodostaa moduulin nimeltä crate crate:n moduulirakenteen juuressa, jota kutsutaan moduulipuuksi.

Listaus 7-2 näyttää moduulipuun listauksen 7-1 rakenteelle.

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
Listing 7-2: Moduulipuu listauksen 7-1 koodille

Tämä puu näyttää, miten jotkin moduulit sisäkkäistyvät muihin moduuleihin; esimerkiksi hosting sisäkkäistyy front_of_house-moduuliin. Puu näyttää myös, että jotkin moduulit ovat sisaruksia, eli ne on määritelty samassa moduulissa; hosting ja serving ovat sisaruksia, jotka on määritelty front_of_house-moduulissa. Jos moduuli A on moduulin B sisällä, sanomme, että moduuli A on moduulin B lapsi ja moduuli B on moduulin A vanhempi. Huomaa, että koko moduulipuu on juurrutettu implisiittiseen moduuliin nimeltä crate.

Moduulipuu saattaa muistuttaa tietokoneesi tiedostojärjestelmän hakemistopuuta; tämä on erittäin osuva vertaus! Aivan kuten hakemistot tiedostojärjestelmässä, käytät moduuleja koodisi organisointiin. Ja aivan kuten tiedostot hakemistossa, tarvitsemme tavan löytää moduulimme.

Polut moduulipuun kohteen viittaamiseen

Polut kohteen viittaamiseen moduulipuussa

Näyttääksemme Rustille, mistä kohteen moduulipuussa löytyy, käytämme polkua samalla tavalla kuin käytämme polkua navigoidessamme tiedostojärjestelmässä. Kutsuaksemme funktiota meidän on tiedettävä sen polku.

Polku voi olla kahdessa muodossa:

  • Absoluuttinen polku on täydellinen polku, joka alkaa crate-juuresta; ulkoisen crate:n koodille absoluuttinen polku alkaa crate:n nimellä, ja nykyisen crate:n koodille se alkaa literaalilla crate.
  • Suhteellinen polku alkaa nykyisestä moduulista ja käyttää self-, super- tai nykyisen moduulin tunnistetta.

Sekä absoluuttisia että suhteellisia polkuja seuraa yksi tai useampi tunniste, jotka on erotettu kaksoispisteillä (::).

Palataan listaukseen 7-1 ja oletetaan, että haluamme kutsua add_to_waitlist-funktiota. Tämä on sama kuin kysyisi: Mikä on add_to_waitlist-funktion polku? Listaus 7-3 sisältää listauksen 7-1, josta on poistettu joitakin moduuleja ja funktioita.

Näytämme kaksi tapaa kutsua add_to_waitlist-funktiota uudesta funktiosta eat_at_restaurant, joka on määritelty crate-juuressa. Nämä polut ovat oikein, mutta on vielä toinen ongelma, joka estää tämän esimerkin kääntymisen sellaisenaan. Selitämme syyn hetken kuluttua.

eat_at_restaurant-funktio on osa kirjastocrate:amme julkista API:a, joten merkitsemme sen pub-avainsanalla. Kohdassa ”Polkujen paljastaminen pub-avainsanalla” käsittelemme pub:ia tarkemmin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-03/src/lib.rs}}
Listing 7-3: add_to_waitlist-funktion kutsuminen absoluuttisilla ja suhteellisilla poluilla

Ensimmäisellä kerralla kun kutsumme add_to_waitlist-funktiota funktiossa eat_at_restaurant, käytämme absoluuttista polkua. add_to_waitlist-funktio on määritelty samassa crate:ssa kuin eat_at_restaurant, mikä tarkoittaa, että voimme käyttää crate-avainsanaa absoluuttisen polun aloittamiseen. Sisällytämme sitten jokaisen peräkkäisen moduulin, kunnes pääsemme add_to_waitlist-funktioon. Voit kuvitella tiedostojärjestelmän, jolla on sama rakenne: Määrittäisimme polun /front_of_house/hosting/add_to_waitlist suorittaaksemme add_to_waitlist-ohjelman; crate-nimen käyttäminen crate-juuresta aloittamiseen on kuin /-merkin käyttäminen tiedostojärjestelmän juuresta aloittamiseen komentorivilläsi.

Toisella kerralla kun kutsumme add_to_waitlist-funktiota funktiossa eat_at_restaurant, käytämme suhteellista polkua. Polku alkaa front_of_house-moduulin nimellä, joka on määritelty samalla tasolla moduulipuussa kuin eat_at_restaurant. Tässä tiedostojärjestelmän vastine olisi polun front_of_house/hosting/add_to_waitlist käyttäminen. Moduulin nimellä aloittaminen tarkoittaa, että polku on suhteellinen.

Valinta suhteellisen tai absoluuttisen polun käytöstä on päätös, jonka teet projektisi perusteella, ja se riippuu siitä, siirrätkö todennäköisemmin kohteen määrittelykoodia erikseen vai yhdessä koodin kanssa, joka käyttää kohdetta. Esimerkiksi jos siirtäisimme front_of_house-moduulin ja eat_at_restaurant-funktion moduuliin nimeltä customer_experience, meidän pitäisi päivittää absoluuttinen polku add_to_waitlist-funktioon, mutta suhteellinen polku olisi edelleen kelvollinen. Jos kuitenkin siirtäisimme eat_at_restaurant-funktion erikseen moduuliin nimeltä dining, absoluuttinen polku add_to_waitlist-kutsuun pysyisi samana, mutta suhteellinen polku pitäisi päivittää. Yleinen mieltymyksemme on määrittää absoluuttiset polut, koska on todennäköisempää, että haluamme siirtää koodin määrittelyjä ja kohteiden kutsuja toisistaan riippumatta.

Yritetään kääntää listaus 7-3 ja selvitetään, miksi se ei vielä käänny! Virheet, jotka saamme, on näytetty listauksessa 7-4.

{{#include ../listings/ch07-managing-growing-projects/listing-07-03/output.txt}}
Listing 7-4: Kääntäjän virheet listauksen 7-3 koodin rakentamisesta

Virheilmoitukset sanovat, että moduuli hosting on yksityinen. Toisin sanoen meillä on oikeat polut hosting-moduuliin ja add_to_waitlist-funktioon, mutta Rust ei anna meidän käyttää niitä, koska sillä ei ole pääsyä yksityisiin osiin. Rustissa kaikki kohteet (funktiot, metodit, structit, enumit, moduulit ja vakiot) ovat oletuksena yksityisiä emomoduuleilleen. Jos haluat tehdä kohteen, kuten funktion tai structin, yksityiseksi, laitat sen moduuliin.

Emomoduulin kohteet eivät voi käyttää alimoduuulien yksityisiä kohteita, mutta alimoduuulien kohteet voivat käyttää esi-isämoduuliensa kohteita. Tämä johtuu siitä, että alimoduuulit käärivät ja piilottavat toteutustietonsa, mutta alimoduuulit näkevät kontekstin, jossa ne on määritelty. Jatkaaksemme metaforaamme, ajattele yksityisyyssääntöjä kuin ravintolan takaosaa: Siellä tapahtuva on yksityistä ravintolan asiakkaille, mutta toimistopäälliköt näkevät ja voivat tehdä kaiken ravintolassa, jota he hallinnoivat.

Rust päätti, että moduulijärjestelmä toimii tällä tavalla, jotta sisäisten toteutustietojen piilottaminen on oletus. Näin tiedät, mitä sisäisen koodin osia voit muuttaa rikkomatta ulkoista koodia. Rust antaa kuitenkin mahdollisuuden paljastaa alimoduuulien koodin sisäisiä osia ulommille esi-isämoduuleille käyttämällä pub-avainsanaa kohteen tekemiseksi julkiseksi.

Polkujen paljastaminen pub-avainsanalla

Palataan listauksen 7-4 virheeseen, joka kertoi, että hosting-moduuli on yksityinen. Haluamme, että emomoduulin eat_at_restaurant-funktiolla on pääsy alimoduuulin add_to_waitlist-funktioon, joten merkitsemme hosting-moduulin pub-avainsanalla, kuten listauksessa 7-5.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-05/src/lib.rs:here}}
Listing 7-5: hosting-moduulin määrittely pub:ksi sen käyttämiseksi funktiosta eat_at_restaurant

Valitettavasti listauksen 7-5 koodi tuottaa edelleen kääntäjän virheitä, kuten listauksessa 7-6.

{{#include ../listings/ch07-managing-growing-projects/listing-07-05/output.txt}}
Listing 7-6: Kääntäjän virheet listauksen 7-5 koodin rakentamisesta

Mitä tapahtui? pub-avainsanan lisääminen mod hosting -lauseen eteen tekee moduulista julkisen. Tämän muutoksen jälkeen, jos pääsemme front_of_house-moduuliin, pääsemme hosting-moduuliin. Mutta hosting-moduulin sisältö on edelleen yksityistä; moduulin tekeminen julkiseksi ei tee sen sisällöstä julkista. pub-avainsana moduulissa sallii vain sen esi-isämoduulien koodin viitata siihen, ei päästä sen sisäiseen koodiin. Koska moduulit ovat säiliöitä, pelkällä moduulin tekemisellä julkiseksi emme voi tehdä paljon; meidän on mentävä pidemmälle ja valittava tehdä yksi tai useampi moduulin sisällä olevista kohteista julkisiksi.

Listauksen 7-6 virheet sanovat, että add_to_waitlist-funktio on yksityinen. Yksityisyyssäännöt koskevat structeja, enumeja, funktioita ja metodeja sekä moduuleja.

Tehdään myös add_to_waitlist-funktiosta julkinen lisäämällä pub-avainsana sen määrittelyn eteen, kuten listauksessa 7-7.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-07/src/lib.rs:here}}
Listing 7-7: pub-avainsanan lisääminen mod hosting - ja fn add_to_waitlist -lauseisiin sallii funktion kutsumisen funktiosta eat_at_restaurant.

Nyt koodi kääntyy! Nähdäksemme, miksi pub-avainsanan lisääminen sallii näiden polkujen käytön funktiossa eat_at_restaurant yksityisyyssääntöjen suhteen, tarkastellaan absoluuttista ja suhteellista polkua.

Absoluuttisessa polussa aloitamme crate:sta, crate:amme moduulipuun juuresta. front_of_house-moduuli on määritelty crate-juuressa. Vaikka front_of_house ei ole julkinen, koska eat_at_restaurant-funktio on määritelty samassa moduulissa kuin front_of_house (eli eat_at_restaurant ja front_of_house ovat sisaruksia), voimme viitata front_of_house-moduuliin funktiosta eat_at_restaurant. Seuraavaksi on hosting-moduuli, joka on merkitty pub:lla. Pääsemme hosting-moduulin emomoduuliin, joten pääsemme hosting-moduuliin. Lopuksi add_to_waitlist-funktio on merkitty pub:lla, ja pääsemme sen emomoduuliin, joten tämä funktiokutsu toimii!

Suhteellisessa polussa logiikka on sama kuin absoluuttisessa polussa paitsi ensimmäisessä vaiheessa: Sen sijaan, että aloittaisimme crate-juuresta, polku alkaa front_of_house-moduulista. front_of_house-moduuli on määritelty samassa moduulissa kuin eat_at_restaurant, joten suhteellinen polku, joka alkaa moduulista, jossa eat_at_restaurant on määritelty, toimii. Sitten, koska hosting ja add_to_waitlist on merkitty pub:lla, polun loppuosa toimii, ja tämä funktiokutsu on kelvollinen!

Jos aiot jakaa kirjastocrate:asi, jotta muut projektit voivat käyttää koodiasi, julkinen API:si on sopimus crate:si käyttäjien kanssa, joka määrittää, miten he voivat olla vuorovaikutuksessa koodisi kanssa. Julkisen API:n muutosten hallintaan liittyy monia näkökohtia, jotka helpottavat ihmisten riippuvuutta crate:stasi. Nämä näkökohdat ylittävät tämän kirjan laajuuden; jos olet kiinnostunut tästä aiheesta, katso Rust API Guidelines -ohjeet.

Parhaat käytännöt paketeille, joissa on binääri ja kirjasto

Mainitsimme, että paketti voi sisältää sekä src/main.rs binääricrate-juuren että src/lib.rs kirjastocrate-juuren, ja molemmilla crate:illa on oletuksena paketin nimi. Tyypillisesti paketit, joissa on tämä malli sisältäen sekä kirjasto- että binääricrate:n, sisältävät binääricrate:ssa vain tarpeeksi koodia käynnistääkseen suoritettavan, joka kutsuu kirjastocrate:ssa määriteltyä koodia. Tämä antaa muiden projektien hyötyä suurimmasta osasta paketin tarjoamaa toiminnallisuutta, koska kirjastocrate:n koodia voidaan jakaa.

Moduulipuu tulisi määritellä tiedostossa src/lib.rs. Sitten mitä tahansa julkisia kohteita voidaan käyttää binääricrate:ssa aloittamalla polut paketin nimellä. Binääricrate:sta tulee kirjastocrate:n käyttäjä aivan kuten täysin ulkoinen crate käyttäisi kirjastocrate:a: se voi käyttää vain julkista API:a. Tämä auttaa suunnittelemaan hyvän API:n; et ole vain kirjoittaja, vaan myös asiakas!

Luvussa 12 havainnollistamme tätä organisointikäytäntöä komentoriviohjelmalla, joka sisältää sekä binääri- että kirjastocrate:n.

Suhteellisten polkujen aloittaminen super:lla

Voimme rakentaa suhteellisia polkuja, jotka alkavat emomoduulista nykyisen moduulin tai crate-juuren sijaan käyttämällä super-avainsanaa polun alussa. Tämä on kuin tiedostojärjestelmän polun aloittaminen ..-syntaksilla, joka tarkoittaa siirtymistä emohakemistoon. super:n käyttäminen sallii meidän viitata kohteeseen, jonka tiedämme olevan emomoduulissa, mikä voi helpottaa moduulipuun uudelleenjärjestelyä, kun moduuli on läheisesti liittynyt emomoduuliin, mutta emomoduuli saatetaan joskus siirtää muualle moduulipuussa.

Harkitse listauksen 7-8 koodia, joka mallintaa tilannetta, jossa keittiömestari korjaa virheellisen tilauksen ja tuo sen henkilökohtaisesti asiakkaalle. back_of_house-moduulissa määritelty fix_incorrect_order-funktio kutsuu emomoduulissa määriteltyä deliver_order-funktiota määrittämällä polun deliver_order-funktioon aloittaen super:lla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-08/src/lib.rs}}
Listing 7-8: Funktion kutsuminen suhteellisella polulla, joka alkaa super:lla

fix_incorrect_order-funktio on back_of_house-moduulissa, joten voimme käyttää super:a siirtyäksemme back_of_house-moduulin emomoduuliin, joka tässä tapauksessa on crate, juuri. Sieltä etsimme deliver_order-funktiota ja löydämme sen. Onnistui! Uskomme, että back_of_house-moduuli ja deliver_order-funktio pysyvät todennäköisesti samassa suhteessa toisiinsa ja siirtyvät yhdessä, jos päättäisimme järjestää crate:n moduulipuun uudelleen. Siksi käytimme super:a, jotta meillä olisi vähemmän paikkoja päivitettäväksi tulevaisuudessa, jos tämä koodi siirretään eri moduuliin.

Structien ja enumien tekeminen julkisiksi

Voimme myös käyttää pub:ia structien ja enumien merkitsemiseen julkisiksi, mutta pub:n käytössä structien ja enumien kanssa on muutamia lisätietoja. Jos käytämme pub:ia struct-määrittelyn edessä, teemme structista julkisen, mutta structin kentät ovat edelleen yksityisiä. Voimme tehdä jokaisesta kentästä julkisen tai yksityisen tapauskohtaisesti. Listauksessa 7-9 olemme määritelleet julkisen back_of_house::Breakfast-structin, jossa on julkinen toast-kenttä mutta yksityinen seasonal_fruit-kenttä. Tämä mallintaa ravintolatilannetta, jossa asiakas voi valita aterian kanssa tulevan leivän tyypin, mutta keittiömestari päättää, mikä hedelmä seuraa ateriaa sen perusteella, mikä on sesongissa ja varastossa. Saatavilla olevat hedelmät vaihtuvat nopeasti, joten asiakkaat eivät voi valita hedelmää tai edes nähdä, minkä hedelmän he saavat.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-09/src/lib.rs}}
Listing 7-9: Struct, jossa on joitakin julkisia ja joitakin yksityisiä kenttiä

Koska back_of_house::Breakfast-structin toast-kenttä on julkinen, funktiossa eat_at_restaurant voimme kirjoittaa ja lukea toast-kenttää käyttämällä pistesyntaksia. Huomaa, ettemme voi käyttää seasonal_fruit-kenttää funktiossa eat_at_restaurant, koska seasonal_fruit on yksityinen. Kokeile poistaa kommentti riviltä, joka muuttaa seasonal_fruit-kentän arvoa, nähdäksesi minkä virheen saat!

Huomaa myös, että koska back_of_house::Breakfast-structilla on yksityinen kenttä, structin on tarjottava julkinen assosioitu funktio, joka luo Breakfast-instanssin (olemme nimenneet sen tässä summer). Jos Breakfast-structilla ei olisi tällaista funktiota, emme voisi luoda Breakfast-instanssia funktiossa eat_at_restaurant, koska emme voisi asettaa yksityisen seasonal_fruit-kentän arvoa funktiossa eat_at_restaurant.

Sitä vastoin, jos teemme enumista julkisen, kaikista sen varianteista tulee julkisia. Tarvitsemme vain pub-avainsanan enum-avainsanan edessä, kuten listauksessa 7-10.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-10/src/lib.rs}}
Listing 7-10: Enumin määrittely julkiseksi tekee kaikista sen varianteista julkisia.

Koska teimme Appetizer-enumista julkisen, voimme käyttää Soup- ja Salad-variantteja funktiossa eat_at_restaurant.

Enumit eivät ole kovin hyödyllisiä, elleivät niiden variantit ole julkisia; olisi ärsyttävää joutua merkitsemään kaikki enum-variantit pub:lla joka tapauksessa, joten enum-varianttien oletus on olla julkisia. Structit ovat usein hyödyllisiä ilman, että niiden kentät olisivat julkisia, joten struct-kentät noudattavat yleistä sääntöä, että kaikki on oletuksena yksityistä, ellei niitä ole merkitty pub:lla.

On vielä yksi tilanne, jossa pub:ia käytetään, jota emme ole käsitelleet, ja se on viimeinen moduulijärjestelmämme ominaisuus: use-avainsana. Käsittelemme use:a ensin erikseen, ja sitten näytämme, miten pub ja use yhdistetään.

Polkujen tuominen näkyvyysalueeseen use-avainsanalla

Polkujen tuominen laajuuteen use-avainsanalla

Polkujen kirjoittaminen funktioiden kutsumiseksi voi tuntua hankalalta ja toistavalta. Listauksessa 7-7, valitsimmepa absoluuttisen tai suhteellisen polun add_to_waitlist-funktioon, meidän piti aina määrittää myös front_of_house ja hosting joka kerta kun halusimme kutsua add_to_waitlist-funktiota. Onneksi on olemassa tapa yksinkertaistaa tätä prosessia: Voimme luoda oikotien polkuun use-avainsanalla kerran ja käyttää sitten lyhyempää nimeä kaikkialla muualla laajuudessa.

Listauksessa 7-11 tuomme crate::front_of_house::hosting-moduulin eat_at_restaurant-funktion laajuuteen, jotta meidän tarvitsee vain määrittää hosting::add_to_waitlist kutsuaksemme add_to_waitlist-funktiota funktiossa eat_at_restaurant.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-11/src/lib.rs}}
Listing 7-11: Moduulin tuominen laajuuteen use:lla

use-avainsanan ja polun lisääminen laajuuteen on samanlaista kuin symbolisen linkin luominen tiedostojärjestelmässä. Lisäämällä use crate::front_of_house::hosting crate-juureen, hosting on nyt kelvollinen nimi kyseisessä laajuudessa, aivan kuin hosting-moduuli olisi määritelty crate-juuressa. use:lla laajuuteen tuodut polut tarkistavat myös yksityisyyden, kuten kaikki muutkin polut.

Huomaa, että use luo oikotien vain sille laajuudelle, jossa use esiintyy. Listaus 7-12 siirtää eat_at_restaurant-funktion uuteen alimoduuuliin nimeltä customer, joka on sitten eri laajuus kuin use-lause, joten funktion runko ei käänny.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-12/src/lib.rs}}
Listing 7-12: use-lause pätee vain laajuudessa, jossa se on.

Kääntäjän virhe näyttää, että oikotie ei enää päde customer-moduulissa:

{{#include ../listings/ch07-managing-growing-projects/listing-07-12/output.txt}}

Huomaa, että on myös varoitus, että use ei ole enää käytössä laajuudessaan! Korjataksesi tämän ongelman, siirrä use myös customer-moduuliin, tai viittaa emomoduulin oikotiehen super::hosting:lla customer-alimoduuulissa.

Idiomatisten use-polkujen luominen

Listauksessa 7-11 saatoit ihmetellä, miksi määritimme use crate::front_of_house::hosting ja kutsuimme sitten hosting::add_to_waitlist funktiossa eat_at_restaurant, sen sijaan että määrittäisimme use-polun aina add_to_waitlist-funktioon saakka saavuttaaksemme saman tuloksen, kuten listauksessa 7-13.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-13/src/lib.rs}}
Listing 7-13: add_to_waitlist-funktion tuominen laajuuteen use:lla, mikä ei ole idiomatista

Vaikka sekä listaus 7-11 että listaus 7-13 saavuttavat saman tehtävän, listaus 7-11 on idiomatinen tapa tuoda funktio laajuuteen use:lla. Funktion emomoduulin tuominen laajuuteen use:lla tarkoittaa, että meidän on määritettävä emomoduuli funktiota kutsuessamme. Emomoduulin määrittäminen funktiota kutsuttaessa tekee selväksi, että funktiota ei ole määritelty paikallisesti, mutta silti minimoidaan täyden polun toisto. Listauksen 7-13 koodi ei ole selvä siitä, missä add_to_waitlist on määritelty.

Toisaalta, kun tuomme structeja, enumeja ja muita kohteita use:lla, on idiomatista määrittää täydellinen polku. Listaus 7-14 näyttää idiomatisen tavan tuoda standardikirjaston HashMap-struct binääricrate:n laajuuteen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-14/src/main.rs}}
}
Listing 7-14: HashMap:n tuominen laajuuteen idiomatiseen tapaan

Tämän idiomien takana ei ole vahvaa syytä: se on vain käytäntö, joka on kehittynyt, ja ihmiset ovat tottuneet lukemaan ja kirjoittamaan Rust-koodia tällä tavalla.

Poikkeus tähän idiomiaan on, jos tuomme kaksi samannimistä kohdetta laajuuteen use-lauseilla, koska Rust ei salli sitä. Listaus 7-15 näyttää, miten tuoda kaksi samannimistä Result-tyyppiä laajuuteen, joilla on sama nimi mutta eri emomoduulit, ja miten viitata niihin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-15/src/lib.rs:here}}
Listing 7-15: Kahden samannimisen tyypin tuominen samaan laajuuteen vaatii niiden emomoduulien käyttämistä.

Kuten näet, emomoduulien käyttäminen erottaa kaksi Result-tyyppiä. Jos sen sijaan määrittäisimme use std::fmt::Result ja use std::io::Result, meillä olisi kaksi Result-tyyppiä samassa laajuudessa, eikä Rust tietäisi, mitä tarkoitimme käyttäessämme Result:ia.

Uusien nimien antaminen as-avainsanalla

On olemassa toinen ratkaisu ongelmaan, jossa tuodaan kaksi samannimistä tyyppiä samaan laajuuteen use:lla: Polun jälkeen voimme määrittää as ja uuden paikallisen nimen, eli aliaksen, tyypille. Listaus 7-16 näyttää toisen tavan kirjoittaa listauksen 7-15 koodi uudelleennimeämällä yksi kahdesta Result-tyypistä as:lla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-16/src/lib.rs:here}}
Listing 7-16: Tyypin uudelleennimeäminen, kun se tuodaan laajuuteen as-avainsanalla

Toisessa use-lauseessa valitsimme uuden nimen IoResult tyypille std::io::Result, joka ei ole ristiriidassa std::fmt:n Result:n kanssa, jonka olemme myös tuoneet laajuuteen. Listaukset 7-15 ja 7-16 katsotaan idiomatiseksi, joten valinta on sinun!

Nimien uudelleenvienti pub use:lla

Kun tuomme nimen laajuuteen use-avainsanalla, nimi on yksityinen laajuudelle, johon sen tuimme. Jotta koodi kyseisen laajuuden ulkopuolelta voisi viitata kyseiseen nimeen ikään kuin se olisi määritelty kyseisessä laajuudessa, voimme yhdistää pub ja use. Tätä tekniikkaa kutsutaan uudelleenvienniksi, koska tuomme kohteen laajuuteen mutta teemme sen myös muiden saataville heidän tuodakseen sen omaan laajuuteensa.

Listaus 7-17 näyttää listauksen 7-11 koodin, jossa juurimoduulin use on muutettu muotoon pub use.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-17/src/lib.rs}}
Listing 7-17: Nimen tekeminen minkä tahansa koodin käytettäväksi uudesta laajuudesta pub use:lla

Ennen tätä muutosta ulkoisen koodin olisi pitänyt kutsua add_to_waitlist-funktiota polulla restaurant::front_of_house::hosting::add_to_waitlist(), mikä olisi myös vaatinut front_of_house-moduulin merkitsemistä pub:ksi. Nyt kun tämä pub use on vienyt uudelleen hosting-moduulin juurimoduulista, ulkoinen koodi voi käyttää polkua restaurant::hosting::add_to_waitlist() sen sijaan.

Uudelleenvienti on hyödyllistä, kun koodisi sisäinen rakenne eroaa siitä, miten koodiasi kutsuvat ohjelmoijat ajattelevat toimialuetta. Esimerkiksi tässä ravintolametaforassa ravintolaa pyörittävät ihmiset ajattelevat ”etuosaa” ja ”takaosaa”. Mutta ravintolaa vierailevat asiakkaat eivät todennäköisesti ajattele ravintolan osia näillä termeillä. pub use:lla voimme kirjoittaa koodimme yhdellä rakenteella mutta paljastaa eri rakenteen. Näin kirjastomme on hyvin organisoitu sekä kirjastoa työstäville ohjelmoijille että kirjastoa kutsuville ohjelmoijille. Tarkastelemme toista esimerkkiä pub use:sta ja sen vaikutuksesta crate:si dokumentaatioon kohdassa ”Kätevän julkisen API:n vienti” luvussa 14.

Ulkoisten pakettien käyttö

Luvussa 2 ohjelmoimme arvauspeli-projektin, joka käytti ulkoista pakettia nimeltä rand satunnaisten lukujen saamiseksi. Käyttääksemme rand:ia projektissamme lisäsimme tämän rivin tiedostoon Cargo.toml:

Filename: Cargo.toml
{{#include ../listings/ch02-guessing-game-tutorial/listing-02-02/Cargo.toml:9:}}

rand:in lisääminen riippuvuudeksi tiedostoon Cargo.toml kertoo Cargolle ladata rand-paketti ja kaikki sen riippuvuudet osoitteesta crates.io ja tehdä rand saataville projektillemme.

Sitten tuodaksemme rand-määrittelyt pakettimme laajuuteen lisäsimme use-rivin, joka alkaa crate:n nimellä rand, ja listasimme kohteet, jotka halusimme tuoda laajuuteen. Muista, että kohdassa ”Satunnaisen luvun generointi” luvussa 2 toimme rand::prelude-moduulin kohteet laajuuteen ja kutsuimme rand::rng-funktiota:

{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-03/src/main.rs:ch07-04}}

Rust-yhteisön jäsenet ovat tehneet monia paketteja saataville osoitteessa crates.io, ja minkä tahansa niistä tuominen pakettiisi sisältää samat vaiheet: niiden listaaminen pakettisi Cargo.toml-tiedostossa ja use:n käyttäminen niiden crate:jen kohteiden tuomiseen laajuuteen.

Huomaa, että standardikirjasto std on myös crate, joka on ulkopuolinen paketillemme. Koska standardikirjasto toimitetaan Rust-kielen mukana, meidän ei tarvitse muuttaa Cargo.toml:ia sisällyttääksemme std:n. Meidän on kuitenkin viitattava siihen use:lla tuodaksemme kohteita sieltä pakettimme laajuuteen. Esimerkiksi HashMap:n kanssa käyttäisimme tätä riviä:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

Tämä on absoluuttinen polku, joka alkaa std:llä, standardikirjaston crate:n nimellä.

Sisäkkäisten polkujen käyttö use-listojen siistimiseen

Jos käytämme useita kohteita, jotka on määritelty samassa crate:ssa tai samassa moduulissa, jokaisen kohteen listaaminen omalle rivilleen voi viedä paljon pystysuuntaista tilaa tiedostoissamme. Esimerkiksi nämä kaksi use-lausetta, joita käytimme arvauspelissä listauksessa 2-4, tuovat std:n kohteet laajuuteen:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/no-listing-01-use-std-unnested/src/main.rs:here}}

Sen sijaan voimme käyttää sisäkkäisiä polkuja tuodaksemme samat kohteet laajuuteen yhdellä rivillä. Teemme tämän määrittämällä polun yhteisen osan, jota seuraa kaksoispiste, ja sitten aaltosulkeet luettelon polkujen osista, jotka eroavat, kuten listauksessa 7-18.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-18/src/main.rs:here}}
Listing 7-18: Sisäkkäisen polun määrittely useiden saman etuliitteen omaavien kohteiden tuomiseksi laajuuteen

Suuremmissa ohjelmissa useiden kohteiden tuominen laajuuteen samasta crate:sta tai moduulista sisäkkäisiä polkuja käyttäen voi vähentää tarvittavien erillisten use-lausetten määrää huomattavasti!

Voimme käyttää sisäkkäistä polkua milla tahansa polun tasolla, mikä on hyödyllistä yhdistettäessä kahta use-lausetta, joilla on yhteinen alipolku. Esimerkiksi listaus 7-19 näyttää kaksi use-lausetta: toinen, joka tuo std::io:n laajuuteen, ja toinen, joka tuo std::io::Write:n laajuuteen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-19/src/lib.rs}}
Listing 7-19: Kaksi use-lausetta, joista toinen on toisen alipolku

Näiden kahden polun yhteinen osa on std::io, ja se on ensimmäisen polun kokonaisuus. Yhdistääksemme nämä kaksi polkua yhdeksi use-lauseeksi voimme käyttää self:ä sisäkkäisessä polussa, kuten listauksessa 7-20.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-20/src/lib.rs}}
Listing 7-20: Listauksen 7-19 polkujen yhdistäminen yhdeksi use-lauseeksi

Tämä rivi tuo std::io:n ja std::io::Write:n laajuuteen.

Kohteiden tuominen glob-operaattorilla

Jos haluamme tuoda kaikki polussa määritellyt julkiset kohteet laajuuteen, voimme määrittää kyseisen polun, jota seuraa *-glob-operaattori:

#![allow(unused)]
fn main() {
use std::collections::*;
}

Tämä use-lause tuo kaikki std::collections:ssa määritellyt julkiset kohteet nykyiseen laajuuteen. Ole varovainen käyttäessäsi glob-operaattoria! Glob voi vaikeuttaa sen tunnistamista, mitkä nimet ovat laajuudessa ja mistä ohjelmassasi käytetty nimi on määritelty. Lisäksi, jos riippuvuus muuttaa määrittelyjään, tuomasi muuttuvat myös, mikä voi johtaa kääntäjän virheisiin riippuvuutta päivitettäessä, jos riippuvuus lisää määrittelyn samalla nimellä kuin sinun määrittelysi samassa laajuudessa, esimerkiksi.

Glob-operaattoria käytetään usein testauksessa tuomaan kaikki testattava tests-moduuliin; käsittelemme tätä kohdassa ”Testien kirjoittaminen” luvussa 11. Glob-operaattoria käytetään joskus myös osana preludi-mallia: Katso standardikirjaston dokumentaatiosta lisätietoja tästä mallista.

Moduulien erottaminen eri tiedostoihin

Moduulien erottaminen eri tiedostoihin

Tähän asti kaikki esimerkit tässä luvussa ovat määrittäneet useita moduuleja yhdessä tiedostossa. Kun moduulit kasvavat suuriksi, saatat haluta siirtää niiden määritelmät erillisiin tiedostoihin, jotta koodia olisi helpompi selata.

Esimerkiksi voimme aloittaa luvun 7-17 koodista, jossa oli useita ravintolaan liittyviä moduuleja. Siirrämme nämä moduulit erillisiin tiedostoihin sen sijaan, että määrittelisimme ne ohjelmakokonaisuuden juuritiedostossa. Tässä tapauksessa juuritiedosto on src/lib.rs, mutta sama menetelmä toimii myös binäärikokonaisuuksissa, joiden juuritiedosto on src/main.rs.

Ensimmäiseksi siirrämme front_of_house-moduulin omaan tiedostoonsa. Poista sen koodilohkon sisältö ja jätä vain mod front_of_house; -määrittely, jolloin src/lib.rs näyttää seuraavalta:

{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-21-and-22/src/lib.rs}}

Huomaa, että tämä ei käänny ennen kuin luomme tiedoston src/front_of_house.rs:

{{#rustdoc_include ../listings/ch07-managing-growing-projects/listing-07-21-and-22/src/front_of_house.rs}}

Rust tietää etsiä tätä tiedostoa, koska se löysi mod front_of_house;-määrittelyn ohjelmakokonaisuuden juurimoduulissa.

Huomioi, että mod-määrittely tarvitaan vain yhden kerran moduulipuussa. Kun kääntäjä tietää, että tiedosto kuuluu projektiin ja missä moduulipuussa se sijaitsee, muiden tiedostojen tulisi viitata siihen samalla tavoin kuin muihin moduuleihin – kuten käsiteltiin luvussa Polut moduulipuussa viittaamiseen. mod ei siis toimi kuten #include joissakin muissa ohjelmointikielissä.

Seuraavaksi siirrämme hosting-moduulin omaan tiedostoonsa. Koska hosting on front_of_house-moduulin lapsimoduuli eikä juurimoduuli, prosessi on hieman erilainen. Luomme hosting-tiedoston src/front_of_house-hakemistoon.

Aloitamme muuttamalla src/front_of_house.rs-tiedostoa niin, että se sisältää vain hosting-moduulin määrittelyn:

{{#rustdoc_include ../listings/ch07-managing-growing-projects/no-listing-02-extracting-hosting/src/front_of_house.rs}}

Seuraavaksi luomme uuden src/front_of_house/hosting.rs-tiedoston ja siirrämme hosting-moduulin koodin sinne:

{{#rustdoc_include ../listings/ch07-managing-growing-projects/no-listing-02-extracting-hosting/src/front_of_house/hosting.rs}}

Jos hosting.rs sijaitsisi suoraan src-hakemistossa, kääntäjä olettaisi sen olevan ohjelmakokonaisuuden juurimoduulin hosting-moduuli eikä front_of_house-moduulin lapsimoduuli. Tämä tarkoittaa, että kääntäjän säännöt tiedostojen sijoittelusta vastaavat moduulipuuta.

Vaihtoehtoiset tiedostopolut

Rust tukee myös vanhempaa tiedostorakennetta. Esimerkiksi front_of_house-moduuli voi sijaita seuraavissa paikoissa:

  • src/front_of_house.rs (nykyinen suositeltu tyyli)
  • src/front_of_house/mod.rs (vanhempi tapa, jota yhä tuetaan)

Vastaavasti hosting-moduuli voi sijaita:

  • src/front_of_house/hosting.rs (nykyinen suositeltu tyyli)
  • src/front_of_house/hosting/mod.rs (vanhempi tapa, jota yhä tuetaan)

Jos käytät molempia tapoja samalle moduulille, saat käännösvirheen. Sekä vanhan että uuden tyylin käyttäminen eri moduuleissa samassa projektissa on sallittua, mutta voi tehdä projektin rakenteen vaikeasti ymmärrettäväksi.

Vanhan tyylin suurin haitta on, että projektissa voi olla useita mod.rs-tiedostoja, mikä voi tehdä niiden käsittelystä vaikeaa, kun useita tiedostoja on auki samanaikaisesti.

Nyt jokainen moduuli on siirretty omaan tiedostoonsa, mutta moduulipuu pysyy muuttumattomana. eat_at_restaurant-funktion kutsut toimivat edelleen ilman muutoksia, vaikka määrittelyt ovat eri tiedostoissa. Tämä menetelmä mahdollistaa moduulien siirtämisen uusiin tiedostoihin niiden kasvaessa ilman, että koodia tarvitsee muuttaa merkittävästi.

Huomaa, että src/lib.rs:ssä oleva pub use crate::front_of_house::hosting-määrittely ei ole muuttunut, eikä use vaikuta siihen, mitkä tiedostot käännetään osana ohjelmakokonaisuutta. mod-avainsana määrittää moduulit, ja Rust etsii moduulin nimeä vastaavasta tiedostosta sen koodin.

Yhteenveto

Rust mahdollistaa paketin jakamisen useisiin ohjelmakokonaisuuksiin (crates) ja ohjelmakokonaisuuden jakamisen moduuleihin, jotta eri moduuleihin määriteltyihin kohteisiin voidaan viitata toisista moduuleista. Tämä voidaan tehdä käyttämällä absoluuttisia tai suhteellisia polkuja. use-avainsanalla polku voidaan tuoda laajuuteen, jolloin sitä voidaan käyttää lyhyemmällä muodolla. Moduulin koodi on oletuksena yksityistä, mutta sen määrittelyt voidaan tehdä julkisiksi lisäämällä pub-avainsana.

Seuraavassa luvussa tutustumme Rustin standardikirjaston kokoelmatietorakenteisiin, joita voit käyttää järjestelmällisessä koodissasi.

Yleiset kokoelmat

Rustin standardikirjasto sisältää joukon erittäin hyödyllisiä tietorakenteita, joita kutsutaan kokoelmiksi (collections). Useimmat muut tietotyypit edustavat yksittäistä arvoa, mutta kokoelmat voivat sisältää useita arvoja. Toisin kuin sisäänrakennetut taulukot (arrays) ja tuplet (tuples), näiden kokoelmien osoittama data tallennetaan kekoon (heap). Tämä tarkoittaa, että tietomäärää ei tarvitse tietää kääntäjän aikana, ja se voi kasvaa tai pienentyä ohjelman suorituksen aikana. Jokaisella kokoelmalla on omat ominaisuutensa ja kustannuksensa, ja sopivan kokoelman valitseminen kuhunkin tilanteeseen on taito, jonka kehität ajan myötä. Tässä luvussa käsittelemme kolmea Rust-ohjelmissa usein käytettyä kokoelmaa:

  • Vektori (vector) mahdollistaa muuttuvan määrän arvoja, jotka sijaitsevat vierekkäin muistissa.
  • Merkkijono (string) on kokoelma merkkejä. Olemme aiemmin maininneet String-tyypin, mutta tässä luvussa perehdymme siihen tarkemmin.
  • Hajautustaulu (hash map) mahdollistaa arvojen yhdistämisen tiettyihin avaimiin. Se on erityinen toteutus yleisemmästä tietorakenteesta, jota kutsutaan mapiksi.

Jos haluat oppia lisää Rustin tarjoamista muista kokoelmista, katso dokumentaatio.

Tässä luvussa käymme läpi, miten luodaan ja päivitetään vektoreita, merkkijonoja ja hajautustauluja sekä mikä tekee kustakin erityisen.

Arvojen listojen tallentaminen vektoreilla

Arvojen listojen tallentaminen vektoreilla

Ensimmäinen kokoelmatyyppi, jota tarkastelemme, on Vec<T>, joka tunnetaan myös vektorina. Vektorit antavat sinun tallentaa useamman kuin yhden arvon yhteen tietorakenteeseen, joka sijoittaa kaikki arvot vierekkäin muistiin. Vektorit voivat tallentaa vain saman tyyppisiä arvoja. Ne ovat hyödyllisiä, kun sinulla on luettelo kohteita, kuten tiedoston tekstirivit tai ostoskorin tuotteiden hinnat.

Uuden vektorin luominen

Luodaksesi uuden tyhjän vektorin kutsumme Vec::new-funktiota, kuten listauksessa 8-1.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-01/src/main.rs:here}}
}
Listing 8-1: Uuden tyhjän vektorin luominen i32-tyyppisten arvojen tallentamiseksi

Huomaa, että lisäsimme tähän tyyppimerkinnän. Koska emme lisää mitään arvoja tähän vektoriin, Rust ei tiedä, millaisia elementtejä aiot tallentaa. Tämä on tärkeä kohta. Vektorit on toteutettu geneerisillä tyypeillä; käsittelemme geneeristen tyyppien käyttöä omissa tyypeissäsi luvussa 10. Toistaiseksi tiedä, että standardikirjaston tarjoama Vec<T>-tyyppi voi tallentaa minkä tahansa tyypin. Kun luomme vektorin tietyn tyypin tallentamiseksi, voimme määrittää tyypin kulmasuluissa. Listauksessa 8-1 olemme kertoneet Rustille, että v:n Vec<T> tallentaa i32-tyyppisiä elementtejä.

Useammin luot Vec<T>:n alkuarvoilla, ja Rust päättelee tallennettavan arvon tyypin, joten harvoin tarvitset tehdä tämän tyyppimerkinnän. Rust tarjoaa kätevästi vec!-makron, joka luo uuden vektorin, joka tallentaa antamasi arvot. Listaus 8-2 luo uuden Vec<i32>:n, joka tallentaa arvot 1, 2 ja 3. Kokonaislukutyyppi on i32, koska se on oletuskokonaislukutyyppi, kuten käsittelimme ”Tietotyypit” -kohdassa luvussa 3.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-02/src/main.rs:here}}
}
Listing 8-2: Uuden arvoja sisältävän vektorin luominen

Koska olemme antaneet alkuperäiset i32-arvot, Rust voi päätellä, että v:n tyyppi on Vec<i32>, eikä tyyppimerkintää tarvita. Seuraavaksi tarkastelemme, miten vektoria muokataan.

Vektorin päivittäminen

Luodaksesi vektorin ja lisätäksesi siihen elementtejä voimme käyttää push-metodia, kuten listauksessa 8-3.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-03/src/main.rs:here}}
}
Listing 8-3: push-metodin käyttö arvojen lisäämiseksi vektoriin

Kuten minkä tahansa muuttujan kanssa, jos haluamme pystyä muuttamaan sen arvoa, meidän on tehtävä se muuttuvaksi mut-avainsanalla, kuten käsittelimme luvussa 3. Sisällyttämämme luvut ovat kaikki i32-tyyppiä, ja Rust päättelee tämän datasta, joten Vec<i32>-merkintää ei tarvita.

Vektorin elementtien lukeminen

On kaksi tapaa viitata vektoriin tallennettuun arvoon: indeksoinnilla tai get-metodilla. Seuraavissa esimerkeissä olemme merkinneet näiden funktioiden palauttamat arvot selkeyden vuoksi.

Listaus 8-4 näyttää molemmat tavat käyttää arvoa vektorissa, indeksointisyntaksilla ja get-metodilla.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-04/src/main.rs:here}}
}
Listing 8-4: Indeksointisyntaksin ja get-metodin käyttö vektorin kohteen käyttämiseksi

Huomaa muutamia yksityiskohtia. Käytämme indeksiarvoa 2 saadaksemme kolmannen elementin, koska vektorit indeksoidaan numerolla alkaen nollasta. &- ja []-merkkien käyttö antaa meille viitteen elementtiin indeksiarvolla. Kun käytämme get-metodia indeksin välittämiseksi argumenttina, saamme Option<&T>:n, jota voimme käyttää match:in kanssa.

Rust tarjoaa nämä kaksi tapaa viitata elementtiin, jotta voit valita, miten ohjelma käyttäytyy, kun yrität käyttää indeksiarvoa olemassa olevien elementtien alueen ulkopuolella. Esimerkkinä katsotaan, mitä tapahtuu, kun meillä on viisi elementtiä sisältävä vektori ja yritämme sitten käyttää elementtiä indeksissä 100 kummallakin tekniikalla, kuten listauksessa 8-5.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-05/src/main.rs:here}}
}
Listing 8-5: Yritys käyttää elementtiä indeksissä 100 vektorissa, jossa on viisi elementtiä

Kun suoritamme tämän koodin, ensimmäinen []-metodi aiheuttaa ohjelman paniikin, koska se viittaa olemattomaan elementtiin. Tätä metodia kannattaa käyttää, kun haluat ohjelmasi kaatuvan, jos yritetään käyttää elementtiä vektorin lopun jälkeen.

Kun get-metodille välitetään indeksi, joka on vektorin ulkopuolella, se palauttaa None:n panikoimatta. Käyttäisit tätä metodia, jos elementin käyttäminen vektorin alueen ulkopuolella voi tapahtua satunnaisesti normaaleissa olosuhteissa. Koodissasi on sitten logiikka käsitellä joko Some(&element) tai None, kuten käsittelimme luvussa 6. Esimerkiksi indeksi voi tulla käyttäjältä, joka syöttää numeron. Jos he vahingossa syöttävät liian suuren numeron ja ohjelma saa None-arvon, voisit kertoa käyttäjälle, kuinka monta kohdetta nykyisessä vektorissa on, ja antaa heille uuden mahdollisuuden syöttää kelvollinen arvo. Se olisi käyttäjäystävällisempää kuin ohjelman kaatuminen kirjoitusvirheen vuoksi!

Kun ohjelmalla on kelvollinen viite, lainauskontrolleri valvoo omistus- ja lainaussääntöjä (käsiteltiin luvussa 4) varmistaakseen, että tämä viite ja kaikki muut viitteet vektorin sisältöön pysyvät kelvollisina. Muista sääntö, jonka mukaan et voi olla muuttuvia ja muuttumattomia viitteitä samassa laajuudessa. Tämä sääntö pätee listauksessa 8-6, jossa pidämme muuttumatonta viitettä vektorin ensimmäiseen elementtiin ja yritämme lisätä elementin loppuun. Tämä ohjelma ei toimi, jos yritämme myös viitata kyseiseen elementtiin myöhemmin funktiossa.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-06/src/main.rs:here}}
Listing 8-6: Yritys lisätä elementti vektoriin samalla kun pidetään viitettä kohteeseen

Tämän koodin kääntäminen tuottaa tämän virheen:

{{#include ../listings/ch08-common-collections/listing-08-06/output.txt}}

Listauksen 8-6 koodi saattaa näyttää siltä, että sen pitäisi toimia: Miksi viitteen ensimmäiseen elementtiin pitäisi välittää muutoksista vektorin lopussa? Tämä virhe johtuu siitä, miten vektorit toimivat: Koska vektorit sijoittavat arvot vierekkäin muistiin, uuden elementin lisääminen vektorin loppuun saattaa vaatia uuden muistin varaamista ja vanhojen elementtien kopioimista uuteen tilaan, jos ei ole tarpeeksi tilaa sijoittaa kaikki elementit vierekkäin sinne, missä vektori on tällä hetkellä tallennettuna. Siinä tapauksessa viite ensimmäiseen elementtiin osoittaisi vapautettuun muistiin. Lainaussäännöt estävät ohjelmia päätymästä tällaiseen tilanteeseen.

Huom: Lisätietoja Vec<T>-tyypin toteutustiedoista on kohdassa ”Rustonomicon”.

Vektorin arvojen läpikäynti

Käyttääksemme jokaista vektorin elementtiä vuorollaan, käymme läpi kaikki elementit sen sijaan, että käyttäisimme indeksejä yksi kerrallaan. Listaus 8-7 näyttää, miten käyttää for-silmukkaa saadaksesi muuttumattomia viitteitä jokaiseen i32-arvojen vektorin elementtiin ja tulostaaksesi ne.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-07/src/main.rs:here}}
}
Listing 8-7: Jokaisen vektorin elementin tulostaminen käymällä elementit läpi for-silmukalla

Voimme myös käydä läpi muuttuvia viitteitä jokaiseen muuttuvan vektorin elementtiin tehdäksemme muutoksia kaikkiin elementteihin. Listauksen 8-8 for-silmukka lisää 50 jokaiseen elementtiin.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-08/src/main.rs:here}}
}
Listing 8-8: Muuttuvien viitteiden läpikäynti vektorin elementeissä

Muuttaaksemme arvoa, johon muuttuva viite viittaa, meidän on käytettävä *-dereferointioperaattoria päästäksemme i:n arvoon ennen kuin voimme käyttää +=-operaattoria. Käsittelemme dereferointioperaattoria tarkemmin kohdassa ”Viittauksen seuraaminen arvoon” luvussa 15.

Vektorin läpikäynti, olipa muuttumattomasti tai muuttuvasti, on turvallista lainauskontrollerin sääntöjen ansiosta. Jos yrittäisimme lisätä tai poistaa kohteita listauksen 8-7 ja listauksen 8-8 for-silmukoiden rungoissa, saisimme kääntäjän virheen, joka on samanlainen kuin listauksen 8-6 koodilla. for-silmukan pitämä viite vektoriin estää koko vektorin samanaikaisen muokkaamisen.

Enumin käyttö useiden tyyppien tallentamiseen

Vektorit voivat tallentaa vain saman tyyppisiä arvoja. Tämä voi olla hankalaa; on ehdottomasti käyttötapauksia, joissa tarvitaan eri tyyppisten kohteiden luettelon tallentaminen. Onneksi enum-variantit on määritelty saman enum-tyypin alle, joten kun tarvitsemme yhden tyypin edustamaan eri tyyppisiä elementtejä, voimme määritellä ja käyttää enumia!

Esimerkiksi oletetaan, että haluamme saada arvoja taulukkolaskentataulukon riviltä, jossa jotkin rivin sarakkeet sisältävät kokonaislukuja, jotkut liukulukuja ja jotkut merkkijonoja. Voimme määritellä enumin, jonka variantit tallentavat eri arvotyyppejä, ja kaikki enum-variantit katsotaan samaksi tyypiksi: enumin tyypiksi. Sitten voimme luoda vektorin, joka tallentaa kyseisen enumin ja siten lopulta eri tyyppejä. Olemme havainnollistaneet tämän listauksessa 8-9.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-09/src/main.rs:here}}
}
Listing 8-9: Enumin määrittely eri tyyppisten arvojen tallentamiseksi yhdessä vektorissa

Rustin on tiedettävä käännösaikana, mitä tyyppejä vektorissa on, jotta se tietää tarkalleen, kuinka paljon muistia keossa tarvitaan jokaisen elementin tallentamiseen. Meidän on myös oltava eksplisiittisiä siitä, mitkä tyypit ovat sallittuja tässä vektorissa. Jos Rust sallisi vektorin sisältää mitä tahansa tyyppiä, olisi mahdollista, että yksi tai useampi tyypeistä aiheuttaisi virheitä vektorin elementeille suoritettavissa operaatioissa. Enumin ja match-lausekkeen käyttö tarkoittaa, että Rust varmistaa käännösaikana, että jokainen mahdollinen tapaus käsitellään, kuten käsittelimme luvussa 6.

Jos et tiedä kattavaa joukkoa tyyppejä, joita ohjelma saa ajonaikana tallennettavaksi vektoriin, enum-tekniikka ei toimi. Sen sijaan voit käyttää trait-objektia, jota käsittelemme luvussa 18.

Nyt kun olemme käsitelleet joitakin yleisimpiä tapoja käyttää vektoreita, muista tutustua API-dokumentaatioon kaikista hyödyllisistä metodeista, jotka standardikirjasto on määritellyt Vec<T>:lle. Esimerkiksi push:n lisäksi pop-metodi poistaa ja palauttaa viimeisen elementin.

Vektorin pudottaminen pudottaa sen elementit

Kuten mikä tahansa muu struct, vektori vapautetaan, kun se poistuu laajuudesta, kuten listauksessa 8-10.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-10/src/main.rs:here}}
}
Listing 8-10: Vektorin ja sen elementtien pudottamisen paikan näyttäminen

Kun vektori pudotetaan, kaikki sen sisältö pudotetaan myös, eli sen sisältämät kokonaisluvut siivotaan. Lainauskontrolleri varmistaa, että viitteitä vektorin sisältöön käytetään vain niin kauan kuin vektori itse on kelvollinen.

Siirrytään seuraavaan kokoelmatyyppiin: String!

UTF-8-koodattujen tekstien tallentaminen merkkijonoilla

UTF-8-koodatun tekstin tallentaminen merkkijonoilla

Käsittelimme merkkijonoja luvussa 4, mutta tarkastelemme niitä nyt syvällisemmin. Uudet rustilaiset jäävät usein jumiin merkkijonoihin kolmen syyn yhdistelmän vuoksi: Rustin taipumus paljastaa mahdolliset virheet, merkkijonojen oleminen monimutkaisempia tietorakenteita kuin monet ohjelmoijat antavat niille tunnustusta, ja UTF-8. Nämä tekijät yhdistyvät tavalla, joka voi tuntua vaikealta, kun tulet muista ohjelmointikielistä.

Käsittelemme merkkijonoja kokoelmien yhteydessä, koska merkkijonot on toteutettu tavujen kokoelmana plus joitakin metodeja hyödyllisen toiminnallisuuden tarjoamiseksi, kun näitä tavuja tulkitaan tekstinä. Tässä osiossa käsittelemme String-tyypin operaatioita, joita jokaisella kokoelmatyypillä on, kuten luominen, päivittäminen ja lukeminen. Käsittelemme myös tapoja, joilla String eroaa muista kokoelmista, nimittäin sitä, miten String-tyyppiin indeksointi on monimutkaista ihmisten ja tietokoneiden eri tulkintojen vuoksi String-datasta.

Merkkijonojen määrittely

Määrittelemme ensin, mitä tarkoitamme termillä merkkijono. Rustissa on vain yksi merkkijonotyyppi ydinkielessä, joka on merkkijonoviipale str, joka nähdään yleensä lainatussa muodossaan &str. Luvussa 4 käsittelimme merkkijonoviipaleita, jotka ovat viittauksia jossain muualla tallennettuun UTF-8-koodattuun merkkijonodataan. Merkkijonoliteraalit, esimerkiksi, tallennetaan ohjelman binääritiedostoon ja ovat siksi merkkijonoviipaleita.

String-tyyppi, jonka Rustin standardikirjasto tarjoaa eikä ydinkieli koodaa, on kasvava, muuttuva, omistettu, UTF-8-koodattu merkkijonotyyppi. Kun rustilaiset viittaavat Rustissa ”merkkijonoihin”, he saattavat viitata joko String- tai merkkijonoviipaletyyppiin &str, ei vain toiseen näistä tyypeistä. Vaikka tämä osio käsittelee pääasiassa String:ia, molempia tyyppejä käytetään runsaasti Rustin standardikirjastossa, ja sekä String että merkkijonoviipaleet ovat UTF-8-koodattuja.

Uuden merkkijonon luominen

Monet samat operaatiot, jotka ovat saatavilla Vec<T>:n kanssa, ovat saatavilla myös String:in kanssa, koska String on itse asiassa toteutettu tavujen vektorin kääreenä joillakin ylimääräisillä takuilla, rajoituksilla ja ominaisuuksilla. Esimerkki funktiosta, joka toimii samalla tavalla Vec<T>:n ja String:in kanssa, on new-funktio instanssin luomiseen, kuten listauksessa 8-11.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-11/src/main.rs:here}}
}
Listing 8-11: Uuden tyhjän String:in luominen

Tämä rivi luo uuden tyhjän merkkijonon nimeltä s, johon voimme sitten ladata dataa. Usein meillä on joitakin alkudataa, joilla haluamme aloittaa merkkijonon. Tätä varten käytämme to_string-metodia, joka on saatavilla millä tahansa tyypillä, joka toteuttaa Display-traitin, kuten merkkijonoliteraalit. Listaus 8-12 näyttää kaksi esimerkkiä.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-12/src/main.rs:here}}
}
Listing 8-12: to_string-metodin käyttö String:in luomiseksi merkkijonoliteraalista

Tämä koodi luo merkkijonon, joka sisältää initial contents.

Voimme myös käyttää funktiota String::from luodaksemme String:in merkkijonoliteraalista. Listauksen 8-13 koodi on vastaava listauksen 8-12 koodille, joka käyttää to_string:ia.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-13/src/main.rs:here}}
}
Listing 8-13: String::from-funktion käyttö String:in luomiseksi merkkijonoliteraalista

Koska merkkijonoja käytetään niin moniin asioihin, voimme käyttää monia erilaisia geneerisiä API:ja merkkijonoille, mikä tarjoaa meille paljon vaihtoehtoja. Jotkut niistä saattavat vaikuttaa tarpeettomilta, mutta niillä kaikilla on paikkansa! Tässä tapauksessa String::from ja to_string tekevät saman asian, joten kumpi valitset on tyyli- ja luettavuuskysymys.

Muista, että merkkijonot ovat UTF-8-koodattuja, joten voimme sisällyttää niihin mitä tahansa oikein koodattua dataa, kuten listauksessa 8-14.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:here}}
}
Listing 8-14: Tervehdysten tallentaminen eri kielillä merkkijonoihin

Kaikki nämä ovat kelvollisia String-arvoja.

Merkkijonon päivittäminen

String voi kasvaa kooltaan ja sen sisältö voi muuttua, aivan kuten Vec<T>:n sisältö, jos työnnät siihen lisää dataa. Lisäksi voit kätevästi käyttää +-operaattoria tai format!-makroa String-arvojen yhdistämiseen.

Liittäminen push_str:lla tai push:lla

Voimme kasvattaa String:ia käyttämällä push_str-metodia liittääksemme merkkijonoviipaleen, kuten listauksessa 8-15.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-15/src/main.rs:here}}
}
Listing 8-15: Merkkijonoviipaleen liittäminen String:iin push_str-metodilla

Näiden kahden rivin jälkeen s sisältää foobar. push_str-metodi ottaa merkkijonoviipaleen, koska emme välttämättä halua ottaa parametrin omistusta. Esimerkiksi listauksen 8-16 koodissa haluamme pystyä käyttämään s2:ta sen sisällön liittämisen jälkeen s1:een.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-16/src/main.rs:here}}
}
Listing 8-16: Merkkijonoviipaleen käyttö sen sisällön liittämisen jälkeen String:iin

Jos push_str-metodi ottaisi s2:n omistuksen, emme voisi tulostaa sen arvoa viimeisellä rivillä. Tämä koodi kuitenkin toimii odotetusti!

push-metodi ottaa yhden merkin parametrina ja lisää sen String:iin. Listaus 8-17 lisää kirjaimen l String:iin push-metodilla.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-17/src/main.rs:here}}
}
Listing 8-17: Yhden merkin lisääminen String-arvoon push:lla

Tuloksena s sisältää lol.

Yhdistäminen +:lla tai format!:lla

Usein haluat yhdistää kaksi olemassa olevaa merkkijonoa. Yksi tapa tehdä se on käyttää +-operaattoria, kuten listauksessa 8-18.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-18/src/main.rs:here}}
}
Listing 8-18: +-operaattorin käyttö kahden String-arvon yhdistämiseksi uudeksi String-arvoksi

Merkkijono s3 sisältää Hello, world!. Syy siihen, miksi s1 ei ole enää kelvollinen lisäyksen jälkeen, ja syy siihen, miksi käytimme viitettä s2:een, liittyy metodin allekirjoitukseen, jota kutsutaan kun käytämme +-operaattoria. +-operaattori käyttää add-metodia, jonka allekirjoitus näyttää suunnilleen tältä:

fn add(self, s: &str) -> String {

Standardikirjastossa näet add:n määriteltynä geneerisillä tyypeillä ja assosioituilla tyypeillä. Tässä olemme korvanneet konkreettisilla tyypeillä, mikä tapahtuu kun kutsumme tätä metodia String-arvoilla. Käsittelemme geneerisiä tyyppejä luvussa 10. Tämä allekirjoitus antaa meille vihjeet, joita tarvitsemme +-operaattorin hankalien osien ymmärtämiseksi.

Ensinnäkin s2:lla on &, mikä tarkoittaa, että lisäämme viitteen toisesta merkkijonosta ensimmäiseen merkkijonoon. Tämä johtuu add-funktion s-parametrista: Voimme lisätä vain merkkijonoviipaleen String:iin; emme voi lisätä kahta String-arvoa yhteen. Mutta odota—&s2:n tyyppi on &String, ei &str, kuten add:n toisessa parametrissa määritellään. Miksi listaus 8-18 sitten kääntyy?

Syy siihen, miksi voimme käyttää &s2:ta add-kutsussa, on se, että kääntäjä voi pakottaa &String-argumentin muotoon &str. Kun kutsumme add-metodia, Rust käyttää dereferointipakkoa, joka tässä muuttaa &s2:n muotoon &s2[..]. Käsittelemme dereferointipakkoa tarkemmin luvussa 15. Koska add ei ota s-parametrin omistusta, s2 on edelleen kelvollinen String tämän operaation jälkeen.

Toiseksi allekirjoituksesta näemme, että add ottaa self:n omistuksen, koska self:llä ei ole &:ia. Tämä tarkoittaa, että s1 listauksessa 8-18 siirretään add-kutsuun eikä ole enää kelvollinen sen jälkeen. Joten vaikka let s3 = s1 + &s2; näyttää siltä, että se kopioisi molemmat merkkijonot ja loisi uuden, tämä lause itse asiassa ottaa s1:n omistuksen, liittää kopion s2:n sisällöstä ja palauttaa tuloksen omistuksen. Toisin sanoen se näyttää tekevän paljon kopioita, mutta ei tee; toteutus on tehokkaampi kuin kopiointi.

Jos meidän on yhdistettävä useita merkkijonoja, +-operaattorin käyttäytyminen muuttuu hankalaksi:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/no-listing-01-concat-multiple-strings/src/main.rs:here}}
}

Tässä vaiheessa s on tic-tac-toe. Kaikkien +- ja "-merkkien kanssa on vaikea nähdä, mitä tapahtuu. Monimutkaisempien merkkijonoyhdistelmien tekemiseen voimme sen sijaan käyttää format!-makroa:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/no-listing-02-format/src/main.rs:here}}
}

Tämä koodi asettaa myös s:n arvoksi tic-tac-toe. format!-makro toimii kuten println!, mutta tulostaa tulosteen näytölle sen sijaan, että se palauttaa String:in sisällöllä. format!-makroa käyttävä versio on paljon helpompi lukea, ja format!-makron generoima koodi käyttää viitteitä, joten tämä kutsu ei ota omistusta mistään parametreistään.

Merkkijonoon indeksointi

Monissa muissa ohjelmointikielissä yksittäisten merkkien käyttäminen merkkijonosta viittaamalla niihin indeksillä on kelvollinen ja yleinen operaatio. Jos kuitenkin yrität käyttää String:in osia indeksointisyntaksilla Rustissa, saat virheen. Harkitse kelvotonta koodia listauksessa 8-19.

{{#rustdoc_include ../listings/ch08-common-collections/listing-08-19/src/main.rs:here}}
Listing 8-19: Yritys käyttää indeksointisyntaksia String:in kanssa

Tämä koodi tuottaa seuraavan virheen:

{{#include ../listings/ch08-common-collections/listing-08-19/output.txt}}

Virhe kertoo tarinan: Rust-merkkijonot eivät tue indeksointia. Mutta miksi ei? Vastataksemme tähän kysymykseen meidän on käsiteltävä, miten Rust tallentaa merkkijonot muistiin.

Sisäinen esitys

String on kääre Vec<u8>:n ympärillä. Katsotaan joitakin oikein koodattuja UTF-8-esimerkkimerkkijonojamme listauksesta 8-14. Ensin tämä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:spanish}}
}

Tässä tapauksessa len on 4, mikä tarkoittaa, että merkkijonoa "Hola" tallentava vektori on 4 tavua pitkä. Jokainen näistä kirjaimista vie 1 tavun UTF-8-koodauksessa. Seuraava rivi saattaa kuitenkin yllättää (huomaa, että tämä merkkijono alkaa kyrillisen isolla kirjaimella Ze, ei numerolla 3):

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:russian}}
}

Jos sinulta kysyttäisiin, kuinka pitkä merkkijono on, saatat sanoa 12. Itse asiassa Rustin vastaus on 24: se on tavujen määrä, joita tarvitaan ”Здравствуйте”:n koodaamiseen UTF-8:ssa, koska jokainen Unicode-skaalaarvo kyseisessä merkkijonossa vie 2 tavua tallennustilaa. Siksi indeksi merkkijonon tavuissa ei aina vastaa kelvollista Unicode-skaalaarvoa. Havainnollistaaksemme, harkitse tätä kelvotonta Rust-koodia:

let hello = "Здравствуйте";
let answer = &hello[0];

Tiedät jo, että answer ei ole З, ensimmäinen kirjain. Kun З on koodattu UTF-8:ssa, ensimmäinen tavu on 208 ja toinen on 151, joten näyttäisi siltä, että answer:n pitäisi olla 208, mutta 208 ei ole kelvollinen merkki yksinään. 208:n palauttaminen ei todennäköisesti ole sitä, mitä käyttäjä haluaisi, jos he pyytäisivät tämän merkkijonon ensimmäistä kirjainta; kuitenkin se on ainoa data, joka Rustilla on tavuindeksissä 0. Käyttäjät eivät yleensä halua tavuarvoa palautettavaksi, vaikka merkkijono sisältäisi vain latinalaisia kirjaimia: Jos &"hi"[0] olisi kelvollista koodia, joka palauttaisi tavuarvon, se palauttaisi 104:n, ei h:ta.

Vastaus on siis, että odottamattoman arvon palauttamisen ja virheiden estämiseksi, joita ei ehkä huomata heti, Rust ei käännä tätä koodia lainkaan ja estää väärinkäsitykset varhaisessa kehitysvaiheessa.

Tavut, skaalaarvot ja grafeemiklusterit

Toinen kohta UTF-8:sta on, että Rustin näkökulmasta merkkijonoihin on itse asiassa kolme relevanttia tapaa katsoa: tavuina, skaalaarvoina ja grafeemiklustereina (lähin vastine sille, mitä kutsumme kirjaimiksi).

Jos katsomme hindinkielistä sanaa ”नमस्ते”, joka on kirjoitettu devanagari-kirjaimistolla, se tallennetaan u8-arvojen vektorina, joka näyttää tältä:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

Se on 18 tavua ja näin tietokoneet lopulta tallentavat tämän datan. Jos katsomme niitä Unicode-skaalaarvoina, jotka ovat Rustin char-tyyppi, nämä tavut näyttävät tältä:

['न', 'म', 'स', '्', 'त', 'े']

Tässä on kuusi char-arvoa, mutta neljäs ja kuudes eivät ole kirjaimia: ne ovat diakriittisiä merkkejä, jotka eivät ole järkeviä yksinään. Lopuksi, jos katsomme niitä grafeemiklustereina, saisimme sen, mitä ihminen kutsuisi hindinkielisen sanan neljäksi kirjaimiksi:

["न", "म", "स्", "ते"]

Rust tarjoaa erilaisia tapoja tulkita raakaa merkkijonodataa, jota tietokoneet tallentavat, jotta jokainen ohjelma voi valita tarvitsemansa tulkinnan riippumatta siitä, millä ihmiskielellä data on.

Viimeinen syy siihen, miksi Rust ei salli meidän indeksoida String:iin saadaksemme merkin, on se, että indeksointi-operaatioiden odotetaan aina vievän vakiintuneen ajan (O(1)). Mutta tätä suorituskykyä ei ole mahdollista taata String:in kanssa, koska Rustin pitäisi käydä läpi sisältö alusta indeksiin määrittääkseen, kuinka monta kelvollista merkkiä siellä on.

Merkkijonoviipaleiden leikkaaminen

Merkkijonoon indeksointi on usein huono idea, koska ei ole selvää, mikä merkkijonon indeksointi-operaation palautustyypin pitäisi olla: tavu, merkki, grafeemiklusteri vai merkkijonoviipale. Jos todella tarvitset käyttää indeksejä merkkijonoviipaleiden luomiseen, Rust pyytää sinua olemaan tarkempi.

Sen sijaan, että indeksoisit []:lla yhdellä numerolla, voit käyttää []:a alueella luodaksesi merkkijonoviipaleen, joka sisältää tiettyjä tavuja:

#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
}

Tässä s on &str, joka sisältää merkkijonon ensimmäiset 4 tavua. Aiemmin mainitsimme, että jokainen näistä merkeistä oli 2 tavua, mikä tarkoittaa, että s on Зд.

Jos yrittäisimme leikata vain osan merkin tavuista jollakin kuten &hello[0..1], Rust panikoisi ajonaikana samalla tavalla kuin jos virheellistä indeksiä käytettäisiin vektorissa:

{{#include ../listings/ch08-common-collections/output-only-01-not-char-boundary/output.txt}}

Sinun pitäisi olla varovainen luodessasi merkkijonoviipaleita alueilla, koska se voi kaataa ohjelmasi.

Merkkijonojen läpikäynti

Paras tapa käsitellä merkkijonon osia on olla eksplisiittinen siitä, haluatko merkkejä vai tavuja. Yksittäisille Unicode-skaalaarvoille käytä chars-metodia. chars:in kutsuminen merkkijonolla ”Зд” erottelee ja palauttaa kaksi char-tyyppistä arvoa, ja voit käydä tuloksen läpi käyttääksesi jokaista elementtiä:

#![allow(unused)]
fn main() {
for c in "Зд".chars() {
    println!("{c}");
}
}

Tämä koodi tulostaa seuraavan:

З
д

Vaihtoehtoisesti bytes-metodi palauttaa jokaisen raakatuvun, mikä saattaa olla sopivaa toimialallesi:

#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
    println!("{b}");
}
}

Tämä koodi tulostaa 4 tavua, jotka muodostavat tämän merkkijonon:

208
151
208
180

Muista kuitenkin, että kelvolliset Unicode-skaalaarvot voivat koostua useammasta kuin yhdestä tavusta.

Grafeemiklusterien saaminen merkkijonoista, kuten devanagari-kirjaimistolla, on monimutkaista, joten standardikirjasto ei tarjoa tätä toiminnallisuutta. Crate:t ovat saatavilla osoitteessa crates.io, jos tarvitset tätä toiminnallisuutta.

Merkkijonojen monimutkaisuuden käsittely

Yhteenvetona, merkkijonot ovat monimutkaisia. Eri ohjelmointikielet tekevät erilaisia valintoja siitä, miten esittää tämä monimutkaisuus ohjelmoijalle. Rust on valinnut tehdä String-datan oikeasta käsittelystä oletuskäyttäytymisen kaikille Rust-ohjelmille, mikä tarkoittaa, että ohjelmoijien on pohdittava UTF-8-datan käsittelyä etukäteen. Tämä kompromissi paljastaa enemmän merkkijonojen monimutkaisuutta kuin on ilmeistä muissa ohjelmointikielissä, mutta se estää sinua käsittelemästä ei-ASCII-merkkeihin liittyviä virheitä myöhemmin kehityssykliäsi.

Hyvä uutinen on, että standardikirjasto tarjoaa paljon toiminnallisuutta, joka on rakennettu String- ja &str-tyyppien päälle auttamaan käsittelemään näitä monimutkaisia tilanteita oikein. Muista tutustua dokumentaatioon hyödyllisistä metodeista, kuten contains merkkijonon etsimiseen ja replace merkkijonon osien korvaamiseen toisella merkkijonolla.

Siirrytään hieman vähemmän monimutkaiseen asiaan: hajautustaulukot!

Avainten tallentaminen liittyvine arvoineen hajautustaulukoissa

Avaimien tallentaminen liittyvine arvoineen hajautustaulukoissa

Viimeinen yleisistä kokoelmistamme on hajautustaulukko. Tyyppi HashMap<K, V> tallentaa avainten tyyppiä K olevien avainten ja arvojen tyyppiä V olevien arvojen välisen kuvauksen käyttämällä hajautusfunktiota, joka määrittää, miten nämä avaimet ja arvot sijoitetaan muistiin. Monet ohjelmointikielet tukevat tällaista tietorakennetta, mutta ne käyttävät usein eri nimeä, kuten hash, map, object, hash table, dictionary tai associative array, mainitakseni muutamia.

Hajautustaulukot ovat hyödyllisiä, kun haluat hakea dataa ei indeksillä, kuten vektoreilla, vaan avaimella, joka voi olla minkä tahansa tyyppinen. Esimerkiksi pelissä voisit seurata kunkin joukkueen pistemäärää hajautustaulukossa, jossa jokainen avain on joukkueen nimi ja arvot ovat kunkin joukkueen pisteet. Joukkueen nimen perusteella voit hakea sen pistemäärän.

Käymme läpi hajautustaulukoiden perus-API:n tässä osiossa, mutta HashMap<K, V>:lle standardikirjaston määrittelemissä funktioissa piilee paljon muutakin hyödyllistä. Kuten aina, tarkista standardikirjaston dokumentaatio lisätietoja varten.

Uuden hajautustaulukon luominen

Yksi tapa luoda tyhjä hajautustaulukko on käyttää new:ia ja lisätä elementtejä insert:illä. Listauksessa 8-20 seuraamme kahden joukkueen, joiden nimet ovat Blue ja Yellow, pistemääriä. Blue-joukkue aloittaa 10 pisteellä ja Yellow-joukkue 50 pisteellä.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-20/src/main.rs:here}}
}
Listing 8-20: Uuden hajautustaulukon luominen ja avainten ja arvojen lisääminen

Huomaa, että meidän on ensin use:ttava HashMap standardikirjaston collections-osasta. Kolmesta yleisestä kokoelmastamme tämä on vähiten käytetty, joten sitä ei sisällytetä automaattisesti preludiin tuotuihin ominaisuuksiin. Hajautustaulukoilla on myös vähemmän tukea standardikirjastosta; ei ole esimerkiksi sisäänrakennettua makroa niiden rakentamiseen.

Kuten vektorit, hajautustaulukot tallentavat datansa kekoon. Tällä HashMap:illa on avaimia tyyppiä String ja arvoja tyyppiä i32. Kuten vektorit, hajautustaulukot ovat homogeenisia: kaikkien avainten on oltava samaa tyyppiä, ja kaikkien arvojen on oltava samaa tyyppiä.

Hajautustaulukon arvojen käyttö

Voimme saada arvon hajautustaulukosta antamalla sen avaimen get-metodille, kuten listauksessa 8-21.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-21/src/main.rs:here}}
}
Listing 8-21: Blue-joukkueen hajautustaulukkoon tallennetun pistemäärän käyttö

Tässä score:lla on arvo, joka liittyy Blue-joukkueeseen, ja tulos on 10. get-metodi palauttaa Option<&V>:n; jos hajautustaulukossa ei ole arvoa kyseiselle avaimelle, get palauttaa None:n. Tämä ohjelma käsittelee Option:in kutsumalla copied:ia saadakseen Option<i32>:n Option<&i32>:n sijaan, sitten unwrap_or:ia asettaakseen score:n nollaksi, jos scores:issa ei ole merkintää avaimelle.

Voimme käydä läpi jokaisen avain-arvo-parin hajautustaulukossa samalla tavalla kuin vektoreilla käyttämällä for-silmukkaa:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/no-listing-03-iterate-over-hashmap/src/main.rs:here}}
}

Tämä koodi tulostaa jokaisen parin satunnaisessa järjestyksessä:

Yellow: 50
Blue: 10

Omistuksen hallinta hajautustaulukoissa

Copy-traitin toteuttaville tyypeille, kuten i32, arvot kopioidaan hajautustaulukkoon. Omistetuille arvoille, kuten String, arvot siirretään ja hajautustaulukko on niiden omistaja, kuten listauksessa 8-22 havainnollistetaan.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-22/src/main.rs:here}}
}
Listing 8-22: Avainten ja arvojen omistajuuden hajautustaulukossa lisäämisen jälkeen

Emme voi käyttää muuttujia field_name ja field_value sen jälkeen, kun ne on siirretty hajautustaulukkoon insert-kutsulla.

Jos lisäämme viittauksia arvoihin hajautustaulukkoon, arvoja ei siirretä hajautustaulukkoon. Arvojen, joihin viitteet osoittavat, on oltava kelvollisia vähintään niin kauan kuin hajautustaulukko on kelvollinen. Käsittelemme näitä ongelmia tarkemmin kohdassa ”Viittausten validointi elinaikoilla” luvussa 10.

Hajautustaulukon päivittäminen

Vaikka avain-arvo-parien määrä on kasvava, jokaisella yksilöllisellä avaimella voi olla vain yksi siihen liittyvä arvo kerrallaan (mutta ei päinvastoin: esimerkiksi sekä Blue- että Yellow-joukkueella voi olla arvo 10 tallennettuna scores-hajautustaulukkoon).

Kun haluat muuttaa dataa hajautustaulukossa, sinun on päätettävä, miten käsitellä tapaus, jossa avaimella on jo arvo. Voit korvata vanhan arvon uudella arvolla täysin sivuuttaen vanhan arvon. Voit pitää vanhan arvon ja jättää huomiotta uuden arvon, lisäten uuden arvon vain, jos avaimella ei jo ole arvoa. Tai voit yhdistää vanhan arvon ja uuden arvon. Katsotaan, miten tehdä kukin näistä!

Arvon korvaaminen

Jos lisäämme avaimen ja arvon hajautustaulukkoon ja lisäämme sitten saman avaimen eri arvolla, avaimeen liittyvä arvo korvataan. Vaikka listauksen 8-23 koodi kutsuu insert:ia kahdesti, hajautustaulukossa on vain yksi avain-arvo-pari, koska lisäämme Blue-joukkueen avaimen arvon molemmilla kerroilla.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-23/src/main.rs:here}}
}
Listing 8-23: Tiettyyn avaimeen tallennetun arvon korvaaminen

Tämä koodi tulostaa {"Blue": 25}. Alkuperäinen arvo 10 on korvattu.

Avaimen ja arvon lisääminen vain, jos avaimella ei ole arvoa

On yleistä tarkistaa, onko tietyllä avaimella jo arvo hajautustaulukossa, ja sitten toimia seuraavasti: Jos avain on olemassa hajautustaulukossa, olemassa olevan arvon pitäisi pysyä ennallaan; jos avainta ei ole, lisää se ja sille arvo.

Hajautustaulukoilla on tähän erityinen API nimeltä entry, joka ottaa parametrina avaimen, jonka haluat tarkistaa. entry-metodin palautusarvo on enum nimeltä Entry, joka edustaa arvoa, joka saattaa olla olemassa tai ei. Oletetaan, että haluamme tarkistaa, onko Yellow-joukkueen avaimella arvo. Jos ei ole, haluamme lisätä arvon 50, ja sama Blue-joukkueelle. entry-API:ta käyttäen koodi näyttää listaukselta 8-24.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-24/src/main.rs:here}}
}
Listing 8-24: entry-metodin käyttö lisäämiseen vain, jos avaimella ei jo ole arvoa

Entry:n or_insert-metodi on määritelty palauttamaan muuttuva viite vastaavan Entry-avaimen arvoon, jos kyseinen avain on olemassa, ja jos ei, se lisää parametrin uudeksi arvoksi tälle avaimelle ja palauttaa muuttuvan viitteen uuteen arvoon. Tämä tekniikka on paljon siistimpi kuin logiikan kirjoittaminen itse, ja lisäksi se toimii paremmin lainauskontrollerin kanssa.

Listauksen 8-24 koodin suorittaminen tulostaa {"Yellow": 50, "Blue": 10}. Ensimmäinen entry-kutsu lisää Yellow-joukkueen avaimen arvolla 50, koska Yellow-joukkueella ei ole vielä arvoa. Toinen entry-kutsu ei muuta hajautustaulukkoa, koska Blue-joukkueella on jo arvo 10.

Arvon päivittäminen vanhan arvon perusteella

Toinen yleinen käyttötapaus hajautustaulukoille on avaimen arvon hakeminen ja sen päivittäminen vanhan arvon perusteella. Esimerkiksi listaus 8-25 näyttää koodin, joka laskee, kuinka monta kertaa kukin sana esiintyy tekstissä. Käytämme hajautustaulukkoa, jossa sanat ovat avaimia, ja kasvatamme arvoa seurataksemme, kuinka monta kertaa olemme nähneet kyseisen sanan. Jos näemme sanan ensimmäistä kertaa, lisäämme ensin arvon 0.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-25/src/main.rs:here}}
}
Listing 8-25: Sanojen esiintymisten laskeminen hajautustaulukolla, joka tallentaa sanat ja laskurit

Tämä koodi tulostaa {"world": 2, "hello": 1, "wonderful": 1}. Saatat nähdä samat avain-arvo-parit tulostettuna eri järjestyksessä: Muista kohdasta ”Hajautustaulukon arvojen käyttö”, että hajautustaulukon läpikäynti tapahtuu satunnaisessa järjestyksessä.

split_whitespace-metodi palauttaa iteraattorin aliviipaleista, jotka on erotettu välilyönneillä text:n arvosta. or_insert-metodi palauttaa muuttuvan viitteen (&mut V) määritellyn avaimen arvoon. Tässä tallennamme kyseisen muuttuvan viitteen count-muuttujaan, joten arvon asettamiseksi meidän on ensin dereferoitava count asteriskilla (*). Muuttuva viite poistuu laajuudesta for-silmukan lopussa, joten kaikki nämä muutokset ovat turvallisia ja lainaussääntöjen sallimia.

Hajautusfunktiot

Oletuksena HashMap käyttää hajautusfunktiota nimeltä SipHash, joka voi tarjota vastustuskykyä hajautustaulukoihin liittyviä palvelunestohyökkäyksiä (DoS) vastaan1. Tämä ei ole nopein saatavilla oleva hajautusalgoritmi, mutta kompromissi paremman turvallisuuden ja suorituskyvyn laskun välillä on sen arvoinen. Jos profiloit koodisi ja huomaat, että oletushajautusfunktio on liian hidas tarkoituksiisi, voit vaihtaa toiseen funktioon määrittämällä eri hasherin. Hasher on tyyppi, joka toteuttaa BuildHasher-traitin. Käsittelemme trait:eja ja niiden toteuttamista luvussa 10. Sinun ei välttämättä tarvitse toteuttaa omaa hasheria alusta; crates.io -sivustolla on muiden Rust-käyttäjien jakamia kirjastoja, jotka tarjoavat hashereita toteuttaen monia yleisiä hajautusalgoritmeja.

Yhteenveto

Vektorit, merkkijonot ja hajautustaulukot tarjoavat paljon toiminnallisuutta, jota tarvitaan ohjelmissa, kun sinun on tallennettava, käytettävä ja muokattava dataa. Tässä on harjoituksia, joihin sinun pitäisi nyt olla valmis:

  1. Annetulle kokonaislukulistalle käytä vektoria ja palauta mediaani (lajiteltuna arvo keskimmäisessä sijainnissa) ja moodi (arvo, joka esiintyy useimmin; hajautustaulukko on hyödyllinen tässä) listasta.
  2. Muunna merkkijonot siansaksaksi. Jokaisen sanan ensimmäinen konsonantti siirretään sanan loppuun ja lisätään ay, joten first muuttuu muotoon irst-fay. Vokaalilla alkaviin sanoihin lisätään loppuun hay sen sijaan (apple muuttuu muotoon apple-hay). Muista UTF-8-koodauksen yksityiskohdat!
  3. Käyttämällä hajautustaulukkoa ja vektoreita luo tekstipohjainen käyttöliittymä, jonka avulla käyttäjä voi lisätä työntekijöiden nimiä osastoon yrityksessä; esimerkiksi ”Add Sally to Engineering” tai ”Add Amir to Sales.” Anna sitten käyttäjän hakea luettelo kaikista osaston henkilöistä tai kaikista yrityksen henkilöistä osastoittain, lajiteltuna aakkosjärjestykseen.

Standardikirjaston API-dokumentaatio kuvaa vektoreiden, merkkijonojen ja hajautustaulukoiden metodeja, jotka ovat hyödyllisiä näissä harjoituksissa!

Siirrymme monimutkaisempiin ohjelmiin, joissa operaatiot voivat epäonnistua, joten on täydellinen hetki käsitellä virheenkäsittelyä. Teemme sen seuraavaksi!


  1. https://en.wikipedia.org/wiki/SipHash

Virheidenhallinta

Ohjelmistokehityksessä virheet ovat väistämättömiä, joten Rust tarjoaa useita tapoja käsitellä tilanteita, joissa jokin menee pieleen. Usein Rust vaatii sinua huomioimaan mahdollisen virheen ja käsittelemään sen ennen kuin koodisi voidaan kääntää. Tämä tekee ohjelmastasi vankemman, koska virheet havaitaan ja käsitellään ennen kuin koodi päätyy tuotantoon!

Rust jakaa virheet kahteen pääkategoriaan: palautettaviin (recoverable) ja palauttamattomiin (unrecoverable) virheisiin. Palautettavan virheen, kuten tiedostoa ei löydy -virheen, kohdalla haluamme yleensä ilmoittaa ongelmasta käyttäjälle ja yrittää toimintoa uudelleen. Palauttamattomat virheet ovat ohjelmointivirheiden oireita, kuten taulukon rajojen ylitys, ja näissä tapauksissa ohjelman on lopetettava välittömästi.

Useimmat ohjelmointikielet eivät tee eroa näiden kahden virhetyypin välillä, vaan käsittelevät molemmat samalla tavalla esimerkiksi poikkeusten (exceptions) avulla. Rust ei käytä poikkeuksia. Sen sijaan se tarjoaa Result<T, E>-tyypin palautettaville virheille ja panic!-makron, joka pysäyttää ohjelman palauttamattoman virheen kohdatessa.

Tässä luvussa käsittelemme ensin panic!-makron käyttöä ja sen jälkeen Result<T, E>-tyypin käyttöä virheiden käsittelyyn. Lopuksi pohdimme, milloin on järkevää yrittää palautua virheestä ja milloin ohjelma kannattaa pysäyttää kokonaan.

Palautumattomat virheet panic!-komennolla

Palauttamattomat virheet panic!-makrolla

Joskus koodissa tapahtuu jotain, mille emme voi tehdä mitään. Näissä tapauksissa Rust tarjoaa panic!-makron. panic! voidaan laukaista kahdella tavalla: joko suorittamalla virheellinen toiminto (kuten yrittämällä käyttää taulukon rajojen ulkopuolista indeksiä) tai kutsumalla panic!-makroa suoraan.

Molemmissa tapauksissa ohjelma pysäytetään ja tapahtuu paniikkitila (panic state). Oletusarvoisesti tämä tulostaa virheviestin, purkaa pinoon kertyneet kutsut (stack unwinding) ja lopettaa ohjelman. Ympäristömuuttujan avulla voidaan myös näyttää pinojäljitys (backtrace), mikä auttaa selvittämään, mistä virhe johtuu.

Pinon purkaminen (unwinding) vs. ohjelman välitön keskeyttäminen (abort)

Kun panic! tapahtuu, Rust suorittaa oletusarvoisesti pinon purkamisen (unwinding), eli käy funktiokutsut taaksepäin ja siivoaa resurssit. Tämä voi kuitenkin olla raskasta. Vaihtoehtoisesti voit määrittää ohjelman keskeyttämään suoraan (abort), jolloin Rust lopettaa ohjelman ilman siivousta.

Keskeytystilan avulla ohjelman käyttämä muisti siivotaan käyttöjärjestelmän toimesta. Jos haluat tehdä binääritiedostosta mahdollisimman pienen, voit vaihtaa purkamisen keskeytykseen lisäämällä seuraavan asetuksen Cargo.toml-tiedostoon:

[profile.release]
panic = 'abort'

panic!-makron kutsuminen

Tarkastellaan yksinkertaista ohjelmaa, joka kutsuu panic!-makroa:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-01-panic/src/main.rs}}
}

Kun ohjelma ajetaan, tulos näyttää tältä:

{{#include ../listings/ch09-error-handling/no-listing-01-panic/output.txt}}

Viimeiset kaksi riviä sisältävät panic!-viestin ja virheen sijainnin (src/main.rs:2:5 tarkoittaa src/main.rs-tiedoston toista riviä, viidettä merkkiä).

Jos panic! tapahtuu kirjastokoodissa, virheviesti voi näyttää tuntemattomalta. Voimme käyttää pinojäljitystä (backtrace) selvittääksemme, missä virhe tapahtui.

panic! ja pinojäljitys

Katsotaan esimerkkiä, jossa ohjelma yrittää käyttää taulukon rajojen ulkopuolista indeksiä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-01/src/main.rs}}
}

Tässä yritämme käyttää 100. alkiota, mutta vektorissa on vain kolme alkiota. Tämä johtaa panic!-tilaan, koska Rust ei voi palauttaa kelvollista arvoa. Toisin kuin C-kielessä, jossa muistialueen ulkopuolinen luku voi aiheuttaa määrittelemätöntä käyttäytymistä (undefined behavior), Rust estää tällaiset tilanteet pysäyttämällä ohjelman.

Jos ajamme ohjelman, virhe näyttää tältä:

{{#include ../listings/ch09-error-handling/listing-09-01/output.txt}}

Virheilmoitus osoittaa virheellisen indeksin käytön ohjelman main.rs-tiedoston rivillä 4.

Rust tarjoaa pinojäljityksen, jonka avulla voi nähdä, mitkä funktiot johtivat panic!-kutsuun. Voit ottaa sen käyttöön asettamalla ympäristömuuttujan RUST_BACKTRACE=1:

$ RUST_BACKTRACE=1 cargo run

Tuloste näyttää jotain tämän kaltaista:

stack backtrace:
   0: rust_begin_unwind
             at /rustc/.../library/std/src/panicking.rs:692:5
   1: core::panicking::panic_fmt
             at /rustc/.../library/core/src/panicking.rs:75:14
   2: core::panicking::panic_bounds_check
             at /rustc/.../library/core/src/panicking.rs:273:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/.../library/core/src/slice/index.rs:274:10
   4: panic::main
             at ./src/main.rs:4:6

Pinojäljityksessä tärkein rivi on src/main.rs:4:6, koska se osoittaa virheen sijainnin omassa koodissamme. Tämän tiedon avulla voimme korjata virheen estämällä taulukon rajojen ylityksen.

Rust estää useita muistivirheitä, joita esiintyy muissa kielissä, kuten buffer overread -haavoittuvuuksia. Jos yritämme käyttää olematonta taulukon kohtaa, Rust pysäyttää ohjelman, jotta virhe ei aiheuttaisi tietoturvaongelmia.


Seuraavaksi tarkastelemme, miten palautettavat virheet voidaan käsitellä käyttäen Result<T, E>-tyyppiä.

Palautuvat virheet Result-tyypillä

Palautuvat virheet Result:illa

Useimmat virheet eivät ole tarpeeksi vakavia vaatimaan ohjelman täydellistä pysäyttämistä. Joskus kun funktio epäonnistuu, syy on sellainen, jonka voit helposti tulkita ja vastata siihen. Esimerkiksi jos yrität avata tiedoston ja operaatio epäonnistuu, koska tiedostoa ei ole olemassa, saatat haluta luoda tiedoston sen sijaan, että lopettaisit prosessin.

Muista kohdasta ”Mahdollisen epäonnistumisen käsittely Result:illa” luvussa 2, että Result-enum on määritelty kahdella variantilla, Ok ja Err, seuraavasti:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T ja E ovat geneerisiä tyyppiparametreja: käsittelemme geneerisiä tyyppejä tarkemmin luvussa 10. Mitä sinun tarvitsee tietää nyt on, että T edustaa onnistumistapauksessa Ok-variantin sisällä palautettavan arvon tyyppiä, ja E edustaa epäonnistumistapauksessa Err-variantin sisällä palautettavan virheen tyyppiä. Koska Result:illa on nämä geneeriset tyyppiparametrit, voimme käyttää Result-tyyppiä ja siihen määriteltyjä funktioita monissa eri tilanteissa, joissa haluamme palauttaa erilaisia onnistumis- ja virhearvoja.

Kutsutaan funktiota, joka palauttaa Result-arvon, koska funktio voi epäonnistua. Listauksessa 9-3 yritämme avata tiedoston.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-03/src/main.rs}}
}
Listing 9-3: Tiedoston avaaminen

File::open:n palautustyyppi on Result<T, E>. Geneerinen parametri T on täytetty File::open:n toteutuksessa onnistumisarvon tyypillä, std::fs::File, joka on tiedostokahva. Virhearvossa käytetty E-tyyppi on std::io::Error. Tämä palautustyyppi tarkoittaa, että File::open-kutsu voi onnistua ja palauttaa tiedostokahvan, josta voimme lukea tai kirjoittaa. Funktiokutsu voi myös epäonnistua: esimerkiksi tiedostoa ei ehkä ole olemassa, tai meillä ei ehkä ole oikeutta käyttää tiedostoa. File::open-funktiolla on oltava tapa kertoa meille, onnistuiko se vai epäonnistuiko, ja antaa meille joko tiedostokahva tai virhetiedot. Tämä tieto on juuri se, mitä Result-enum välittää.

Tapauksessa, jossa File::open onnistuu, muuttujan greeting_file_result arvo on Ok-instanssi, joka sisältää tiedostokahvan. Tapauksessa, jossa se epäonnistuu, muuttujan greeting_file_result arvo on Err-instanssi, joka sisältää lisätietoja tapahtuneesta virhetyypistä.

Meidän on lisättävä listauksen 9-3 koodiin toimia, jotka riippuvat File::open:n palauttamasta arvosta. Listaus 9-4 näyttää yhden tavan käsitellä Result:ia perustyökalulla, match-lausekkeella, jota käsittelimme luvussa 6.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-04/src/main.rs}}
}
Listing 9-4: match-lausekkeen käyttö palautettavien Result-varianttien käsittelyyn

Huomaa, että kuten Option-enum, Result-enum ja sen variantit on tuotu laajuuteen preludin kautta, joten meidän ei tarvitse määrittää Result:: ennen Ok- ja Err-variantteja match-haaroissa.

Kun tulos on Ok, tämä koodi palauttaa sisäisen file-arvon Ok-variantista, ja sitten määritämme kyseisen tiedostokahvan arvon muuttujalle greeting_file. match:in jälkeen voimme käyttää tiedostokahvaa lukemiseen tai kirjoittamiseen.

match:in toinen haara käsittelee tapauksen, jossa saamme Err-arvon File::open:sta. Tässä esimerkissä olemme valinneet kutsua panic!-makroa. Jos nykyisessä hakemistossamme ei ole tiedostoa nimeltä hello.txt ja suoritamme tämän koodin, näemme seuraavan tulosteen panic!-makrosta:

{{#include ../listings/ch09-error-handling/listing-09-04/output.txt}}

Kuten tavallisesti, tämä tuloste kertoo meille tarkalleen, mikä meni pieleen.

Eri virheiden täsmäyttäminen

Listauksen 9-4 koodi kutsuu panic!:ia riippumatta siitä, miksi File::open epäonnistui. Haluamme kuitenkin toimia eri tavoin eri epäonnistumissyistä. Jos File::open epäonnistui, koska tiedostoa ei ole olemassa, haluamme luoda tiedoston ja palauttaa kahvan uudelle tiedostolle. Jos File::open epäonnistui jostain muusta syystä—esimerkiksi koska meillä ei ollut oikeutta avata tiedostoa—haluamme silti, että koodi kutsuu panic!:ia samalla tavalla kuin listauksessa 9-4. Tätä varten lisäämme sisäisen match-lausekkeen, kuten listauksessa 9-5.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-05/src/main.rs}}
Listing 9-5: Eri virhetyyppien käsittely eri tavoin

File::open:n Err-variantin sisällä palauttaman arvon tyyppi on io::Error, joka on standardikirjaston tarjoama struct. Tällä structilla on metodi kind, jota voimme kutsua saadaksemme io::ErrorKind-arvon. Enum io::ErrorKind on standardikirjaston tarjoama ja sillä on variantteja, jotka edustavat erilaisia virhetyyppejä, jotka voivat johtua io-operaatiosta. Variantti, jota haluamme käyttää, on ErrorKind::NotFound, joka ilmaisee, että tiedostoa, jota yritämme avata, ei ole vielä olemassa. Joten täsmäytämme greeting_file_result:in, mutta meillä on myös sisäinen match error.kind():n päällä.

Ehto, jonka haluamme tarkistaa sisäisessä match:issa, on se, onko error.kind():n palauttama arvo ErrorKind-enumin NotFound-variantti. Jos on, yritämme luoda tiedoston File::create:lla. Koska File::create voi myös epäonnistua, tarvitsemme toisen haaran sisäiseen match-lausekkeeseen. Kun tiedostoa ei voida luoda, tulostetaan eri virheilmoitus. Ulomman match:in toinen haara pysyy samana, joten ohjelma panikoi kaikissa virheissä paitsi puuttuvan tiedoston virheessä.

Vaihtoehdot match:in käytölle Result<T, E>:n kanssa

Se on paljon match:ia! match-lauseke on hyvin hyödyllinen, mutta myös hyvin primitiivinen. Luvussa 13 opit sulkeista, joita käytetään monien Result<T, E>:lle määriteltyjen metodien kanssa. Nämä metodit voivat olla tiiviimpiä kuin match:in käyttö Result<T, E>-arvojen käsittelyssä koodissasi.

Esimerkiksi tässä on toinen tapa kirjoittaa sama logiikka kuin listauksessa 9-5, tällä kertaa käyttäen sulkeita ja unwrap_or_else-metodia:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

Vaikka tämä koodi käyttäytyy samalla tavalla kuin listaus 9-5, siinä ei ole match-lausekkeita ja se on siistimpi lukea. Palaa tähän esimerkkiin luvun 13 jälkeen ja etsi unwrap_or_else-metodi standardikirjaston dokumentaatiosta. Monia muita näistä metodeista voi siivota valtavia, sisäkkäisiä match-lausekkeita, kun käsittelet virheitä.

Oikotiet paniikkiin virheessä

match:in käyttö toimii tarpeeksi hyvin, mutta se voi olla hieman sanallista eikä aina välitä aikomusta hyvin. Result<T, E>-tyypillä on monia apumetodeja, jotka on määritelty erilaisiin, tarkempiin tehtäviin. unwrap-metodi on oikotiemetodi, joka on toteutettu juuri kuten listauksessa 9-4 kirjoittamamme match-lauseke. Jos Result-arvo on Ok-variantti, unwrap palauttaa Ok:n sisällä olevan arvon. Jos Result on Err-variantti, unwrap kutsuu panic!-makroa puolestamme. Tässä on esimerkki unwrap:in toiminnasta:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-04-unwrap/src/main.rs}}
}

Jos suoritamme tämän koodin ilman hello.txt-tiedostoa, näemme virheilmoituksen unwrap-metodin tekemästä panic!-kutsusta:

thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Vastaavasti expect-metodi antaa meille myös valita panic!-virheilmoituksen. expect:in käyttäminen unwrap:in sijaan ja hyvien virheilmoitusten antaminen voi välittää aikomuksesi ja helpottaa paniikin lähteen jäljittämistä. expect:in syntaksi näyttää tältä:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-05-expect/src/main.rs}}
}

Käytämme expect:ia samalla tavalla kuin unwrap:ia: palauttaaksemme tiedostokahvan tai kutsuaksemme panic!-makroa. expect:in panic!-kutsussa käytetty virheilmoitus on parametri, jonka välitämme expect:ille, unwrap:in käyttämän oletusarvoisen panic!-viestin sijaan. Se näyttää tältä:

thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Tuotantolaatuisessa koodissa useimmat rustilaiset valitsevat expect:in unwrap:in sijaan ja antavat enemmän kontekstia siitä, miksi operaation odotetaan aina onnistuvan. Näin, jos oletuksesi koskaan osoittautuvat vääriksi, sinulla on enemmän tietoa virheenkorjaukseen.

Virheiden propagointi

Kun funktion toteutus kutsuu jotain, mikä voi epäonnistua, sen sijaan että käsittelisit virheen funktion sisällä, voit palauttaa virheen kutsuvalle koodille, jotta se voi päättää, mitä tehdä. Tätä kutsutaan virheen propagoinniksi, ja se antaa enemmän hallintaa kutsuvalle koodille, jossa saattaa olla enemmän tietoa tai logiikkaa, joka sanelee, miten virhe pitäisi käsitellä, kuin mitä sinulla on käytettävissäsi koodisi kontekstissa.

Esimerkiksi listaus 9-6 näyttää funktion, joka lukee käyttäjänimen tiedostosta. Jos tiedostoa ei ole olemassa tai sitä ei voida lukea, tämä funktio palauttaa nämä virheet koodille, joka kutsui funktiota.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#include ../listings/ch09-error-handling/listing-09-06/src/main.rs:here}}
}
Listing 9-6: Funktio, joka palauttaa virheet kutsuvalle koodille match:ia käyttäen

Tämä funktio voidaan kirjoittaa paljon lyhyemmällä tavalla, mutta aloitamme tekemällä suuren osan manuaalisesti tutkiaksemme virheenkäsittelyä; lopussa näytämme lyhyemmän tavan. Katsotaan ensin funktion palautustyyppiä: Result<String, io::Error>. Tämä tarkoittaa, että funktio palauttaa arvon tyyppiä Result<T, E>, jossa geneerinen parametri T on täytetty konkreettisella tyypillä String ja geneerinen tyyppi E on täytetty konkreettisella tyypillä io::Error.

Jos tämä funktio onnistuu ilman ongelmia, tätä funktiota kutsuva koodi saa Ok-arvon, joka sisältää String:in—tiedostosta luetun username:n. Jos tämä funktio kohtaa ongelmia, kutsuva koodi saa Err-arvon, joka sisältää io::Error-instanssin, jossa on lisätietoja ongelmista. Valitsimme io::Error:in tämän funktion palautustyypiksi, koska se sattuu olemaan virhearvon tyyppi, jonka molemmat tämän funktion rungossa kutsumamme operaatiot voivat palauttaa: File::open-funktio ja read_to_string-metodi.

Funktion runko alkaa kutsumalla File::open-funktiota. Sitten käsittelemme Result-arvon match:illa, joka on samanlainen kuin listauksen 9-4 match. Jos File::open onnistuu, tiedostokahva kuviomuuttujassa file tulee muuttuvan muuttujan username_file arvoksi ja funktio jatkuu. Err-tapauksessa panic!:in kutsumisen sijaan käytämme return-avainsanaa palataksemme aikaisin kokonaan funktiosta ja välittääksemme File::open:n virhearvon, nyt kuviomuuttujassa e, takaisin kutsuvalle koodille tämän funktion virhearvona.

Joten, jos meillä on tiedostokahva username_file:ssa, funktio luo sitten uuden String:in muuttujaan username ja kutsuu read_to_string-metodia tiedostokahvassa username_file lukeakseen tiedoston sisällön username:iin. read_to_string-metodi palauttaa myös Result:in, koska se voi epäonnistua, vaikka File::open onnistui. Joten tarvitsemme toisen match:in käsitelläksemme kyseisen Result:in: Jos read_to_string onnistuu, funktiomme on onnistunut, ja palautamme tiedostosta nyt username:ssa olevan käyttäjänimen Ok:ssa käärittynä. Jos read_to_string epäonnistuu, palautamme virhearvon samalla tavalla kuin palautimme virhearvon match:issa, joka käsitteli File::open:n palautusarvon. Meidän ei kuitenkaan tarvitse eksplisiittisesti sanoa return, koska tämä on funktion viimeinen lauseke.

Tätä koodia kutsuva koodi käsittelee sitten joko Ok-arvon, joka sisältää käyttäjänimen, tai Err-arvon, joka sisältää io::Error:in. Kutsuvalle koodille jää päätettäväksi, mitä näille arvoille tehdään. Jos kutsuva koodi saa Err-arvon, se voisi kutsua panic!:ia ja kaataa ohjelman, käyttää oletuskäyttäjänimeä tai hakea käyttäjänimen jostain muualta kuin tiedostosta, esimerkiksi. Meillä ei ole tarpeeksi tietoa siitä, mitä kutsuva koodi todella yrittää tehdä, joten propagoidamme kaiken onnistumis- tai virhetiedon ylöspäin sen käsiteltäväksi asianmukaisesti.

Tämä virheiden propagoinnin malli on niin yleinen Rustissa, että Rust tarjoaa kysymysmerkkioperaattorin ? helpottamaan tätä.

?-operaattorin oikotie

Listaus 9-7 näyttää read_username_from_file:n toteutuksen, jolla on sama toiminnallisuus kuin listauksessa 9-6, mutta tässä toteutuksessa käytetään ?-operaattoria.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#include ../listings/ch09-error-handling/listing-09-07/src/main.rs:here}}
}
Listing 9-7: Funktio, joka palauttaa virheet kutsuvalle koodille ?-operaattorilla

Result-arvon jälkeen sijoitettu ? on määritelty toimimaan lähes samalla tavalla kuin match-lausekkeet, jotka määrittelimme käsittelemään Result-arvoja listauksessa 9-6. Jos Result-arvon arvo on Ok, Ok:n sisällä oleva arvo palautetaan tästä lausekkeesta ja ohjelma jatkuu. Jos arvo on Err, Err palautetaan koko funktiosta ikään kuin olisimme käyttäneet return-avainsanaa, jotta virhearvo propagoidaan kutsuvalle koodille.

On ero listauksen 9-6 match-lausekkeen ja ?-operaattorin välillä: Virhearvot, joille ?-operaattoria kutsutaan, käyvät läpi from-funktion, joka on määritelty standardikirjaston From-traitissa ja jota käytetään arvojen muuntamiseen tyypistä toiseen. Kun ?-operaattori kutsuu from-funktiota, vastaanotettu virhetyyppi muunnetaan nykyisen funktion palautustyypissä määriteltyyn virhetyyppiin. Tämä on hyödyllistä, kun funktio palauttaa yhden virhetyypin edustaen kaikkia tapoja, joilla funktio voi epäonnistua, vaikka osat voivat epäonnistua monista eri syistä.

Esimerkiksi voisimme muuttaa listauksen 9-7 read_username_from_file-funktion palauttamaan mukautetun virhetyypin nimeltä OurError, jonka määrittelemme. Jos määrittelemme myös impl From<io::Error> for OurError rakentaaksemme OurError-instanssin io::Error:sta, read_username_from_file-funktion rungon ?-operaattorikutsut kutsuvat from:ia ja muuntavat virhetyypit lisäämättä enempää koodia funktioon.

Listauksen 9-7 kontekstissa File::open-kutsun lopussa oleva ? palauttaa Ok:n sisällä olevan arvon muuttujaan username_file. Jos virhe tapahtuu, ?-operaattori palaa aikaisin koko funktiosta ja antaa minkä tahansa Err-arvon kutsuvalle kodelle. Sama pätee read_to_string-kutsun lopussa olevaan ?:ään.

?-operaattori poistaa paljon toistuvaa koodia ja yksinkertaistaa tämän funktion toteutusta. Voimme jopa lyhentää tätä koodia edelleen ketjuttamalla metodikutsuja heti ?:n jälkeen, kuten listauksessa 9-8.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#include ../listings/ch09-error-handling/listing-09-08/src/main.rs:here}}
}
Listing 9-8: Metodikutsujen ketjuttaminen ?-operaattorin jälkeen

Olemme siirtäneet uuden String:in luomisen username:ssa funktion alkuun; tämä osa ei ole muuttunut. Sen sijaan, että loisimme muuttujan username_file, olemme ketjuttaneet read_to_string-kutsun suoraan File::open("hello.txt")?:n tulokseen. Meillä on edelleen ? read_to_string-kutsun lopussa, ja palautamme edelleen Ok-arvon, joka sisältää username:n, kun sekä File::open että read_to_string onnistuvat virheiden palauttamisen sijaan. Toiminnallisuus on taas sama kuin listauksissa 9-6 ja 9-7; tämä on vain eri, ergonomisempi tapa kirjoittaa se.

Listaus 9-9 näyttää tavan tehdä tämä vielä lyhyemmäksi käyttämällä fs::read_to_string:ia.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#include ../listings/ch09-error-handling/listing-09-09/src/main.rs:here}}
}
Listing 9-9: fs::read_to_string:in käyttö tiedoston avaamisen ja lukemisen sijaan

Tiedoston lukeminen merkkijonoon on melko yleinen operaatio, joten standardikirjasto tarjoaa kätevän fs::read_to_string-funktion, joka avaa tiedoston, luo uuden String:in, lukee tiedoston sisällön, laittaa sisällön kyseiseen String:iin ja palauttaa sen. Tietysti fs::read_to_string:in käyttö ei anna meille mahdollisuutta selittää kaikkea virheenkäsittelyä, joten teimme sen ensin pidemmällä tavalla.

Missä ?-operaattoria voi käyttää

?-operaattoria voi käyttää vain funktioissa, joiden palautustyyppi on yhteensopiva arvon kanssa, jolle ? käytetään. Tämä johtuu siitä, että ?-operaattori on määritelty suorittamaan aikainen paluu arvosta funktiosta samalla tavalla kuin listauksessa 9-6 määrittelemämme match-lauseke. Listauksessa 9-6 match käytti Result-arvoa, ja aikaisen paluun haara palautti Err(e)-arvon. Funktion palautustyypin on oltava Result, jotta se on yhteensopiva tämän return:in kanssa.

Listauksessa 9-10 katsotaan virhettä, jonka saamme, jos käytämme ?-operaattoria main-funktiossa, jonka palautustyyppi ei ole yhteensopiva arvon tyypin kanssa, jolle käytämme ?:ää.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-10/src/main.rs}}
Listing 9-10: ?:n käyttö main-funktiossa, joka palauttaa (), ei käänny.

Tämä koodi avaa tiedoston, mikä voi epäonnistua. ?-operaattori seuraa File::open:n palauttamaa Result-arvoa, mutta tällä main-funktiolla on palautustyyppi (), ei Result. Kun käännetään tämä koodi, saamme seuraavan virheilmoituksen:

{{#include ../listings/ch09-error-handling/listing-09-10/output.txt}}

Tämä virhe osoittaa, että ?-operaattoria saa käyttää vain funktiossa, joka palauttaa Result:in, Option:in tai muun tyypin, joka toteuttaa FromResidual:in.

Virheen korjaamiseksi sinulla on kaksi vaihtoehtoa. Yksi vaihtoehto on muuttaa funktiosi palautustyyppi yhteensopivaksi arvon kanssa, jolle käytät ?-operaattoria, kunhan sinulla ei ole rajoituksia, jotka estävät sen. Toinen vaihtoehto on käyttää match:ia tai jotakin Result<T, E>:n metodeista käsitelläksesi Result<T, E>:n sopivalla tavalla.

Virheilmoitus mainitsi myös, että ?:ää voi käyttää myös Option<T>-arvojen kanssa. Kuten Result:in kanssa, voit käyttää ?:ää Option:issa vain funktiossa, joka palauttaa Option:in. ?-operaattorin käyttäytyminen, kun sitä kutsutaan Option<T>:llä, on samanlainen kuin kun sitä kutsutaan Result<T, E>:llä: Jos arvo on None, None palautetaan aikaisin funktiosta kyseisessä kohdassa. Jos arvo on Some, Some:n sisällä oleva arvo on lausekkeen tulosarvo, ja funktio jatkuu. Listaus 9-11 on esimerkki funktiosta, joka löytää annetun tekstin ensimmäisen rivin viimeisen merkin.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-11/src/main.rs:here}}
}
Listing 9-11: ?-operaattorin käyttö Option<T>-arvolla

Tämä funktio palauttaa Option<char>:in, koska siellä saattaa olla merkki, mutta se on myös mahdollista, ettei siellä ole. Tämä koodi ottaa text-merkkijonoviipaleargumentin ja kutsuu sen lines-metodia, joka palauttaa iteraattorin merkkijonon riveistä. Koska tämä funktio haluaa tarkastella ensimmäistä riviä, se kutsuu next:ia iteraattorilla saadakseen ensimmäisen arvon iteraattorista. Jos text on tyhjä merkkijono, tämä next-kutsu palauttaa None:n, jolloin käytämme ?:a pysähtyäksemme ja palauttaaksemme None:n funktiosta last_char_of_first_line. Jos text ei ole tyhjä merkkijono, next palauttaa Some-arvon, joka sisältää merkkijonoviipaleen ensimmäisestä rivistä text:issä.

? erottaa merkkijonoviipaleen, ja voimme kutsua chars:ia kyseisellä merkkijonoviipaleella saadaksemme iteraattorin sen merkeistä. Olemme kiinnostuneita viimeisestä merkistä tällä ensimmäisellä rivillä, joten kutsumme last:ia palauttaaksemme viimeisen kohteen iteraattorista. Tämä on Option, koska on mahdollista, että ensimmäinen rivi on tyhjä merkkijono; esimerkiksi jos text alkaa tyhjällä rivillä mutta sisältää merkkejä muilla riveillä, kuten "\nhi". Jos kuitenkin ensimmäisellä rivillä on viimeinen merkki, se palautetaan Some-variantissa. Keskellä oleva ?-operaattori antaa meille tiiviin tavan ilmaista tämä logiikka, jolloin voimme toteuttaa funktion yhdellä rivillä. Jos emme voisi käyttää ?-operaattoria Option:issa, meidän pitäisi toteuttaa tämä logiikka useammilla metodikutsuilla tai match-lausekkeella.

Huomaa, että voit käyttää ?-operaattoria Result:issa funktiossa, joka palauttaa Result:in, ja voit käyttää ?-operaattoria Option:issa funktiossa, joka palauttaa Option:in, mutta et voi sekoittaa niitä. ?-operaattori ei automaattisesti muunna Result:ia Option:iksi tai päinvastoin; näissä tapauksissa voit käyttää metodeja, kuten ok-metodia Result:issa tai ok_or-metodia Option:issa, tehdäksesi muunnoksen eksplisiittisesti.

Tähän mennessä kaikki käyttämämme main-funktiot ovat palauttaneet (). main-funktio on erityinen, koska se on suoritettavan ohjelman sisään- ja uloskäyntipiste, ja sen palautustyypillä on rajoituksia, jotta ohjelma käyttäytyy odotetusti.

Onneksi main voi myös palauttaa Result<(), E>. Listaus 9-12 sisältää listauksen 9-10 koodin, mutta olemme muuttaneet main:in palautustyypiksi Result<(), Box<dyn Error>> ja lisänneet paluuarvon Ok(()) loppuun. Tämä koodi kääntyy nyt.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-12/src/main.rs}}
Listing 9-12: main:in muuttaminen palauttamaan Result<(), E> sallii ?-operaattorin käytön Result-arvoilla.

Box<dyn Error>-tyyppi on trait-objekti, jota käsittelemme kohdassa ”Trait-objektien käyttö jaetun käyttäytymisen abstrahointiin” luvussa 18. Toistaiseksi voit lukea Box<dyn Error>:in tarkoittavan ”mitä tahansa virhetyyppiä”. ?:n käyttö Result-arvolla main-funktiossa virhetyypillä Box<dyn Error> on sallittua, koska se sallii minkä tahansa Err-arvon palauttamisen aikaisin. Vaikka tämän main-funktion runko palauttaisi vain std::io::Error-tyyppisiä virheitä, määrittämällä Box<dyn Error> tämä allekirjoitus pysyy oikeana, vaikka main:in runkoon lisättäisiin enemmän koodia, joka palauttaa muita virheitä.

Kun main-funktio palauttaa Result<(), E>, suoritettava lopettaa arvolla 0, jos main palauttaa Ok(()), ja lopettaa nollasta poikkeavalla arvolla, jos main palauttaa Err-arvon. C-kielellä kirjoitetut suoritettavat palauttavat kokonaislukuja lopettaessaan: onnistuneesti lopettavat ohjelmat palauttavat kokonaisluvun 0, ja virheelliset ohjelmat palauttavat jonkin muun kokonaisluvun kuin 0. Rust palauttaa myös kokonaislukuja suoritettavista ollakseen yhteensopiva tämän käytännön kanssa.

main-funktio voi palauttaa mitä tahansa tyyppejä, jotka toteuttavat traitin std::process::Termination, joka sisältää funktion report, joka palauttaa ExitCode:n. Katso standardikirjaston dokumentaatiosta lisätietoja Termination-traitin toteuttamisesta omille tyypeillesi.

Nyt kun olemme käsitelleet panic!:in kutsumisen tai Result:in palauttamisen yksityiskohdat, palataan aiheeseen, miten päättää, kumpaa on sopivaa käyttää missäkin tilanteessa.

panic! vai ei panic!?

panic! vai ei?

Miten siis päätät, milloin sinun pitäisi kutsua panic!:ia ja milloin palauttaa Result? Kun koodi panikoi, palautumista ei ole mahdollista. Voit kutsua panic!:ia mihin tahansa virhetilanteeseen, onko mahdollinen tapa palautua vai ei, mutta silloin teet päätöksen, että tilanne on palautumaton kutsuvalle koodille. Kun valitset palauttaa Result-arvon, annat kutsuvalle koodille vaihtoehtoja. Kutsuva koodi voi valita yrittää palautua tavalla, joka sopii sen tilanteeseen, tai se voi päättää, että Err-arvo tässä tapauksessa on palautumaton, jolloin se voi kutsua panic!:ia ja muuttaa palautuvan virheesi palautumattomaksi. Siksi Result:in palauttaminen on hyvä oletusvalinta, kun määrittelet funktion, joka voi epäonnistua.

Tilanteissa kuten esimerkeissä, prototyyppikoodissa ja testeissä on sopivampaa kirjoittaa koodia, joka panikoi Result:in palauttamisen sijaan. Tutkitaan miksi, ja sitten käsitellään tilanteita, joissa kääntäjä ei voi kertoa, että epäonnistuminen on mahdotonta, mutta sinä ihmisenä voit. Luku päättyy yleisiin ohjeisiin siitä, milloin panikoida kirjastokoodissa.

Esimerkit, prototyyppikoodi ja testit

Kun kirjoitat esimerkkiä havainnollistamaan jotain käsitettä, vankan virheenkäsittelykoodin sisällyttäminen voi tehdä esimerkistä epäselvemmän. Esimerkeissä ymmärretään, että metodin, kuten unwrap:in, kutsuminen, joka voi panikoida, on tarkoitettu paikanpitäjäksi sille tavalle, jolla haluat sovelluksesi käsittelevän virheet, mikä voi vaihdella sen mukaan, mitä muu koodisi tekee.

Vastaavasti unwrap- ja expect-metodit ovat erittäin käteviä prototyyppauksessa, kun et ole vielä valmis päättämään, miten käsitellä virheitä. Ne jättävät selkeät merkit koodiisi siihen asti, kun olet valmis tekemään ohjelmastasi vankemman.

Jos metodikutsu epäonnistuu testissä, haluat koko testin epäonnistuvan, vaikka kyseinen metodi ei olisikaan testattava toiminnallisuus. Koska panic! on tapa, jolla testi merkitään epäonnistuneeksi, unwrap:in tai expect:in kutsuminen on juuri sitä, mitä pitäisi tapahtua.

Kun sinulla on enemmän tietoa kuin kääntäjällä

Olisi myös sopivaa kutsua expect:ia, kun sinulla on muuta logiikkaa, joka varmistaa, että Result:illa on Ok-arvo, mutta logiikka ei ole sellaista, mitä kääntäjä ymmärtää. Sinulla on silti Result-arvo, joka on käsiteltävä: mikä tahansa kutsumasi operaatio voi silti epäonnistua yleisesti, vaikka se olisi loogisesti mahdotonta juuri sinun tilanteessasi. Jos voit varmistaa tarkastamalla koodin manuaalisesti, ettet koskaan saa Err-varianttia, on täysin hyväksyttävää kutsua expect:ia ja dokumentoida syy, miksi uskot, ettei koskaan saa Err-varianttia, argumenttitekstissä. Tässä on esimerkki:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-08-unwrap-that-cant-fail/src/main.rs:here}}
}

Luomme IpAddr-instanssin jäsentämällä kovakoodatun merkkijonon. Näemme, että 127.0.0.1 on kelvollinen IP-osoite, joten expect:in käyttö on tässä hyväksyttävää. Kovakoodatun kelvollisen merkkijonon olemassaolo ei kuitenkaan muuta parse-metodin palautustyyppiä: saamme silti Result-arvon, ja kääntäjä pakottaa meidät käsittelemään Result:ia ikään kuin Err-variantti olisi mahdollisuus, koska kääntäjä ei ole tarpeeksi älykäs nähdäkseen, että tämä merkkijono on aina kelvollinen IP-osoite. Jos IP-osoitemerkkijono tulisi käyttäjältä sen sijaan, että se olisi kovakoodattu ohjelmaan ja siksi voisi epäonnistua, käsittelisimme ehdottomasti Result:ia vankemmalla tavalla. Oletuksen mainitseminen, että tämä IP-osoite on kovakoodattu, kehottaa meitä muuttamaan expect:in paremmaksi virheenkäsittelykoodiksi, jos tulevaisuudessa tarvitsemme hakea IP-osoitteen jostain muusta lähteestä.

Ohjeita virheenkäsittelyyn

On suositeltavaa, että koodisi panikoi, kun on mahdollista, että koodisi päätyy huonoon tilaan. Tässä kontekstissa huono tila tarkoittaa tilannetta, jossa jokin oletus, takuu, sopimus tai invariantti on rikottu, kuten kun virheellisiä arvoja, ristiriitaisia arvoja tai puuttuvia arvoja välitetään koodillesi—plus yksi tai useampi seuraavista:

  • Huono tila on jotain odottamatonta, toisin kuin jotain, mikä todennäköisesti tapahtuu satunnaisesti, kuten käyttäjä syöttää dataa väärässä muodossa.
  • Koodisi tästä eteenpäin on luotettava siihen, ettei ole tässä huonossa tilassa, sen sijaan että tarkistaisi ongelman jokaisessa vaiheessa.
  • Ei ole hyvää tapaa koodata tätä tietoa käyttämiisi tyyppeihin. Käymme läpi esimerkin siitä, mitä tarkoitamme kohdassa ”Tilojen ja käyttäytymisen koodaus tyypeillä” luvussa 18.

Jos joku kutsuu koodiasi ja välittää arvoja, jotka eivät ole järkeviä, on parasta palauttaa virhe, jos voit, jotta kirjastosi käyttäjä voi päättää, mitä tehdä kyseisessä tapauksessa. Jos kuitenkin jatkaminen voisi olla epäturvallista tai haitallista, paras valinta saattaa olla kutsua panic!:ia ja hälyttää kirjastosi käyttäjä heidän koodinsa bugista, jotta he voivat korjata sen kehityksen aikana. Vastaavasti panic! on usein sopivaa, jos kutsut ulkoista koodia, joka on hallintasi ulkopuolella ja palauttaa virheellisen tilan, jota et voi korjata.

Kun epäonnistuminen on odotettavissa, on sopivampaa palauttaa Result kuin kutsua panic!:ia. Esimerkkejä ovat jäsentäjälle annettu virheellinen data tai HTTP-pyyntö, joka palauttaa tilan, joka ilmaisee, että olet osunut nopeusrajoitukseen. Näissä tapauksissa Result:in palauttaminen ilmaisee, että epäonnistuminen on odotettu mahdollisuus, jonka kutsuvan koodin on päätettävä, miten käsitellä.

Kun koodisi suorittaa operaation, joka voi asettaa käyttäjän riskiin, jos sitä kutsutaan virheellisillä arvoilla, koodisi pitäisi ensin varmistaa, että arvot ovat kelvollisia, ja panikoida, jos arvot eivät ole kelvollisia. Tämä on pääasiassa turvallisuussyistä: Yritys käsitellä virheellistä dataa voi altistaa koodisi haavoittuvuuksille. Tämä on pääsyy, miksi standardikirjasto kutsuu panic!:ia, jos yrität muistin käyttöä alueen ulkopuolelta: Yritys käyttää muistia, joka ei kuulu nykyiseen tietorakenteeseen, on yleinen turvallisuusongelma. Funktioilla on usein sopimuksia: Niiden käyttäytymistä taataan vain, jos syötteet täyttävät tietyt vaatimukset. Panikointi, kun sopimus rikotaan, on järkevää, koska sopimusrikkomus osoittaa aina kutsupuolen bugia, eikä se ole sellainen virhe, jonka haluat kutsuvan koodin käsittelevän eksplisiittisesti. Itse asiassa ei ole järkevää tapaa kutsuvalle koodille palautua; kutsupuolen ohjelmoijien on korjattava koodi. Funktion sopimukset, erityisesti kun rikkomus aiheuttaa paniikin, tulisi selittää funktion API-dokumentaatiossa.

Paljon virhetarkistuksia kaikissa funktioissasi olisi kuitenkin sanallista ja ärsyttävää. Onneksi voit käyttää Rustin tyyppijärjestelmää (ja siten kääntäjän tekemää tyyppitarkistusta) tekemään monia tarkistuksista puolestasi. Jos funktiollasi on tietty tyyppi parametrina, voit jatkaa koodisi logiikkaa tietäen, että kääntäjä on jo varmistanut, että sinulla on kelvollinen arvo. Esimerkiksi jos sinulla on tyyppi Option:in sijaan, ohjelmasi odottaa jotain eikä ei mitään. Koodisi ei sitten tarvitse käsitellä kahta tapausta Some- ja None-varianteille: sillä on vain yksi tapaus, jossa arvo on varmasti olemassa. Koodi, joka yrittää välittää ei mitään funktiollesi, ei edes käänny, joten funktiosi ei tarvitse tarkistaa kyseistä tapausta ajonaikana. Toinen esimerkki on etumerkittömän kokonaislukutyypin, kuten u32:n, käyttö, joka varmistaa, että parametri ei ole koskaan negatiivinen.

Mukautetut tyypit validointiin

Viedään ajatus Rustin tyyppijärjestelmän käytöstä kelvollisen arvon varmistamiseksi askeleen pidemmälle ja katsotaan mukautetun tyypin luomista validointiin. Muista arvauspeli luvusta 2, jossa koodimme pyysi käyttäjää arvaamaan luvun välillä 1 ja 100. Emme koskaan validoineet, että käyttäjän arvaus oli näiden lukujen välillä ennen kuin tarkistimme sen salaisen lukuamme vastaan; validoimme vain, että arvaus oli positiivinen. Tässä tapauksessa seuraukset eivät olleet kovin vakavat: Tulosteemme ”Liian korkea” tai ”Liian matala” olisi silti oikein. Mutta olisi hyödyllinen parannus ohjata käyttäjää kohti kelvollisia arvauksia ja saada eri käyttäytyminen, kun käyttäjä arvaa luvun, joka on alueen ulkopuolella, verrattuna tilanteeseen, jossa käyttäjä kirjoittaa esimerkiksi kirjaimia numeroiden sijaan.

Yksi tapa tehdä tämä olisi jäsentää arvaus i32:ksi pelkän u32:n sijaan salliakseen mahdollisesti negatiiviset luvut, ja sitten lisätä tarkistus, että luku on alueella, näin:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-09-guess-out-of-range/src/main.rs:here}}

if-lauseke tarkistaa, onko arvomme alueen ulkopuolella, kertoo käyttäjälle ongelmasta ja kutsuu continue:a aloittaakseen silmukan seuraavan iteraation ja pyytääkseen uuden arvauksen. if-lausekkeen jälkeen voimme jatkaa guess:n ja salaisen luvun vertailuja tietäen, että guess on välillä 1 ja 100.

Tämä ei kuitenkaan ole ihanteellinen ratkaisu: Jos olisi ehdottoman kriittistä, että ohjelma toimisi vain arvoilla välillä 1 ja 100, ja sillä olisi monia funktioita tällä vaatimuksella, tällaisen tarkistuksen tekeminen jokaisessa funktiossa olisi työlästä (ja saattaisi vaikuttaa suorituskykyyn).

Sen sijaan voimme luoda uuden tyypin erilliseen moduuliin ja sijoittaa validoinnit funktioon, joka luo tyypin instanssin, sen sijaan että toistaisimme validoinnit kaikkialla. Näin on turvallista funktioiden käyttää uutta tyyppiä signatuureissaan ja luottaa saamiinsa arvoihin. Listaus 9-13 näyttää yhden tavan määritellä Guess-tyyppi, joka luo Guess-instanssin vain, jos new-funktio saa arvon välillä 1 ja 100.

Filename: src/guessing_game.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-13/src/guessing_game.rs}}
}
Listing 9-13: Guess-tyyppi, joka jatkaa vain arvoilla välillä 1 ja 100

Huomaa, että tämä koodi tiedostossa src/guessing_game.rs riippuu moduulimäärittelyn mod guessing_game; lisäämisestä tiedostoon src/lib.rs, jota emme ole näyttäneet tässä. Tämän uuden moduulin tiedostossa määrittelemme structin nimeltä Guess, jolla on kenttä nimeltä value, joka tallentaa i32:n. Tähän tallennetaan luku.

Sitten toteutamme assosioitun funktion nimeltä new tyypille Guess, joka luo Guess-arvojen instansseja. new-funktio on määritelty ottamaan yksi parametri nimeltä value tyyppiä i32 ja palauttamaan Guess:in. new-funktion rungon koodi testaa value:n varmistaakseen, että se on välillä 1 ja 100. Jos value ei läpäise tätä testiä, kutsumme panic!:ia, mikä hälyttää kutsupuolen ohjelmoijan, että heillä on bugi korjattavana, koska Guess:in luominen value:lla tämän alueen ulkopuolella rikkoisi sopimuksen, johon Guess::new luottaa. Olosuhteet, joissa Guess::new saattaa panikoida, tulisi käsitellä sen julkisessa API-dokumentaatiossa; käsittelemme dokumentointikäytäntöjä, jotka ilmaisevat panic!:in mahdollisuuden luomassasi API-dokumentaatiossa luvussa 14. Jos value läpäisee testin, luomme uuden Guess:in, jonka value-kenttä on asetettu value-parametriin, ja palautamme Guess:in.

Seuraavaksi toteutamme metodin nimeltä value, joka lainaa self:ää, ei ota muita parametreja ja palauttaa i32:n. Tällaisia metodeja kutsutaan joskus gettereiksi, koska niiden tarkoitus on saada dataa kentistään ja palauttaa se. Tämä julkinen metodi on tarpeen, koska Guess-structin value-kenttä on yksityinen. On tärkeää, että value-kenttä on yksityinen, jotta Guess-structia käyttävä koodi ei saa asettaa value:a suoraan: guessing_game-moduulin ulkopuolinen koodi täytyy käyttää Guess::new-funktiota luodakseen Guess-instanssin, varmistaen näin, ettei Guess:illä voi olla value:a, jota Guess::new-funktion ehtoja ei ole tarkistettu.

Funktio, jolla on parametri tai joka palauttaa vain lukuja välillä 1 ja 100, voi sitten ilmoittaa signatuurissaan, että se ottaa tai palauttaa Guess:in i32:n sijaan, eikä sen tarvitse tehdä lisätarkistuksia rungossaan.

Yhteenveto

Rustin virheenkäsittelyominaisuudet on suunniteltu auttamaan sinua kirjoittamaan vankempaa koodia. panic!-makro ilmaisee, että ohjelmasi on tilassa, jota se ei voi käsitellä, ja antaa sinun kertoa prosessille pysähtyä sen sijaan, että yrittäisit jatkaa virheellisillä tai vääriä arvoilla. Result-enum käyttää Rustin tyyppijärjestelmää ilmaisemaan, että operaatiot voivat epäonnistua tavalla, josta koodisi voi palautua. Voit käyttää Result:ia kertomaan koodille, joka kutsuu koodiasi, että sen on käsiteltävä mahdollinen onnistuminen tai epäonnistuminen. panic!:in ja Result:in käyttö sopivissa tilanteissa tekee koodistasi luotettavamman väistämättömien ongelmien edessä.

Nyt kun olet nähnyt hyödyllisiä tapoja, joilla standardikirjasto käyttää geneerisiä tyyppejä Option- ja Result-enumien kanssa, puhumme siitä, miten geneeriset tyypit toimivat ja miten voit käyttää niitä koodissasi.

Geneeriset tyypit, traitit ja eliniät

Jokaisella ohjelmointikielellä on työkaluja, joilla voidaan tehokkaasti vähentää toistuvuutta koodissa. Rustissa yksi tällainen työkalu on geneerisyys (generics): abstraktit paikkamerkit konkreettisille tyypeille tai muille ominaisuuksille. Voimme määrittää geneeristen tyyppien käyttäytymisen tai niiden suhteen toisiin geneerisiin tyyppeihin, tietämättä tarkalleen, mitä niiden tilalla tulee olemaan kääntö- ja suoritusaikana.

Funktiot voivat ottaa parametreja, joiden tyyppi on geneerinen, samalla tavalla kuin ne ottavat parametreja, joiden arvoja ei tunneta etukäteen. Olemme jo käyttäneet geneerisiä tyyppejä aiemmin, esimerkiksi:

  • Luvussa 6 Option<T>
  • Luvussa 8 Vec<T> ja HashMap<K, V>
  • Luvussa 9 Result<T, E>

Tässä luvussa opit määrittelemään omia tyyppejä, funktioita ja metodeja geneerisillä tyypeillä!

Ensin tarkastelemme, miten voimme vähentää koodin toistuvuutta siirtämällä toistuvaa koodia erilliseen funktioon. Tämän jälkeen käytämme samaa tekniikkaa muuntaaksemme kaksi erillistä funktiota geneeriseksi funktioksi. Opimme myös, miten geneerisiä tyyppejä voidaan käyttää rakenteissa (structs) ja luetteloissa (enums).

Seuraavaksi käsittelemme traitteja, joilla voidaan määritellä geneeristä käyttäytymistä. Traitteja voi yhdistää geneeristen tyyppien kanssa, jolloin voidaan rajoittaa, millaiset tyypit ovat kelvollisia.

Lopuksi tarkastelemme elinikiä (lifetimes), jotka ovat geneeristen tyyppien erityismuoto. Ne antavat kääntäjälle tietoa siitä, miten viittaukset liittyvät toisiinsa, varmistaen, että viittaukset pysyvät kelvollisina useammissa tilanteissa.


Toiston poistaminen siirtämällä koodi funktioon

Geneerisyys mahdollistaa tiettyjen tyyppien korvaamisen paikkamerkillä, joka voi edustaa useita eri tyyppejä. Ennen kuin syvennymme geneeriseen syntaksiin, tarkastelemme, kuinka voimme poistaa toistuvaa koodia ilman geneerisiä tyyppejä – siirtämällä se funktioon. Sen jälkeen käytämme samaa menetelmää muuttaaksemme funktiosta geneerisen.

Aloitetaan lyhyellä ohjelmalla, joka etsii suurimman luvun listasta.

Esimerkki: Suurimman luvun etsiminen listasta

Alla oleva ohjelma (Listing 10-1) tallentaa listan kokonaislukuja muuttujaan number_list. Se asettaa ensimmäisen luvun suurimmaksi ja käy sitten kaikki listan luvut läpi. Jos nykyinen luku on suurempi kuin jo löydetty suurin luku, se päivitetään. Lopuksi largest-muuttujaan jää suurin luku, tässä tapauksessa 100.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-01/src/main.rs:here}}
}

Olemme nyt saaneet tehtäväksi löytää suurimman luvun kahdesta eri listasta. Voimme ratkaista tämän kopioimalla saman koodin kahteen eri paikkaan, kuten alla:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-02/src/main.rs}}
}

Vaikka tämä toimii, koodin kopiointi on huono käytäntö, koska se tekee ohjelmasta vaikeammin ylläpidettävän ja virhealttiimman. Jos haluamme muuttaa logiikkaa myöhemmin, meidän on päivitettävä molemmat kopiot.

Parempi ratkaisu on luoda erillinen funktio, joka toimii minkä tahansa listan kanssa. Tämä tekee koodista selkeämmän ja abstraktimman.

Abstraktin funktion määrittely

Alla olevassa esimerkissä (Listing 10-3) siirrämme suurimman luvun etsintälogiikan largest-nimiseen funktioon. Tämän jälkeen kutsumme sitä kahdella eri listalla.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-03/src/main.rs:here}}
}

Tässä largest-funktiolla on parametri list, joka voi edustaa mitä tahansa i32-kokonaislukujen viipaletta. Kun funktiota kutsutaan, se käyttää annettua listaa.

Muutos prosessissa:

  1. Tunnista toistuva koodi.
  2. Siirrä toistuva koodi erilliseen funktioon ja määritä sen parametrit ja palautusarvo.
  3. Kutsu funktiota niissä kohdissa, joissa aiemmin oli toistuvaa koodia.

Seuraavaksi sovellamme samaa lähestymistapaa geneerisiin tyyppeihin, jotta voimme vähentää koodin toistoa entisestään. Samalla tavalla kuin funktiot voivat toimia abstraktilla list-parametrilla, ne voivat myös toimia abstrakteilla tyypeillä.

Esimerkiksi, jos meillä olisi kaksi funktiota:

  • Yksi, joka löytää suurimman arvon i32-listasta.
  • Toinen, joka löytää suurimman arvon char-listasta.

Kuinka voisimme yhdistää nämä funktiot geneerisen tyypin avulla? Siitä jatkamme seuraavaksi!

Geneeriset tietotyypit

Geneeriset tietotyypit

Käytämme geneerisiä tyyppejä luodaksemme määritelmiä esimerkiksi funktioiden allekirjoituksille tai structeille, joita voimme sitten käyttää monilla eri konkreettisilla tietotyypeillä. Aloitetaan katsomalla, miten funktioita, structeja, enum-arvoja ja metodeja määritellään geneeristen tyyppien avulla. Sen jälkeen käsittelemme, miten geneeriset tyypit vaikuttavat koodin suorituskykyyn.

Funktiomäärittelyissä

Kun määrittelemme funktion, joka käyttää geneerisiä tyyppejä, sijoitamme geneeriset tyypit funktion allekirjoitukseen sinne, missä yleensä määrittelemme parametrien ja palautusarvon tietotyypit. Näin koodistamme tulee joustavampaa ja se tarjoaa kutsujille enemmän toiminnallisuutta samalla kun vältämme koodin toistamista.

Jatkamme largest-funktion parissa. Listaus 10-4 näyttää kaksi funktiota, jotka molemmat etsivät suurimman arvon viipaleesta. Yhdistämme ne sitten yhdeksi funktioksi, joka käyttää geneerisiä tyyppejä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-04/src/main.rs:here}}
}
Listing 10-4: Kaksi funktiota, jotka eroavat vain nimissään ja allekirjoituksissa olevissa tyypeissään

Funktio largest_i32 on se, jonka erotimme listauksessa 10-3 ja joka etsii suurimman i32-arvon viipaleesta. Funktio largest_char etsii suurimman char-arvon viipaleesta. Funktioiden rungot ovat samanlaiset, joten poistamme toiston ottamalla käyttöön geneerisen tyyppiparametrin yhdessä funktiossa.

Parametroidaksemme tyypit uudessa yhdessä funktiossa meidän täytyy nimetä tyyppiparametri, aivan kuten teemme arvoparametreille funktiossa. Voit käyttää mitä tahansa tunnistetta tyyppiparametrin nimenä. Käytämme kuitenkin T:tä, koska Rustin käytännön mukaan tyyppiparametrien nimet ovat lyhyitä, usein vain yksi kirjain, ja Rustin tyyppien nimeämiskäytäntö on UpperCamelCase. Lyhenne sanasta type, T on useimpien Rust-ohjelmoijien oletusvalinta.

Kun käytämme parametria funktion rungossa, meidän täytyy ilmoittaa parametrin nimi allekirjoituksessa, jotta kääntäjä tietää, mitä nimi tarkoittaa. Vastaavasti, kun käytämme tyyppiparametrin nimeä funktion allekirjoituksessa, meidän täytyy ilmoittaa tyyppiparametrin nimi ennen kuin käytämme sitä. Määritelläksemme geneerisen largest-funktion sijoitamme tyyppinimien ilmoitukset kulmasulkeisiin <> funktion nimen ja parametrilistan väliin, näin:

fn largest<T>(list: &[T]) -> &T {

Luemme tämän määrittelyn näin: ”Funktio largest on geneerinen jonkin tyypin T suhteen.” Tällä funktiolla on yksi parametri nimeltä list, joka on tyypin T arvojen viipale. Funktio largest palauttaa viittauksen arvoon, jonka tyyppi on sama T.

Listaus 10-5 näyttää yhdistetyn largest-funktion määrittelyn, joka käyttää geneeristä tietotyyppiä allekirjoituksessaan. Listaus näyttää myös, miten funktiota voidaan kutsua joko i32-arvojen tai char-arvojen viipaleella. Huomaa, että tämä koodi ei vielä käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-05/src/main.rs}}
Listing 10-5: largest-funktio, joka käyttää geneerisiä tyyppiparametreja; tämä ei vielä käänny

Jos käännämme tämän koodin nyt, saamme seuraavan virheen:

{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-05/output.txt}}

Ohjeteksti mainitsee std::cmp::PartialOrd, joka on trait, ja käsittelemme traitteja seuraavassa osiossa. Toistaiseksi tiedä, että tämä virhe kertoo, että largest-funktion runko ei toimi kaikille mahdollisille tyypeille, joita T voisi olla. Koska haluamme vertailla tyypin T arvoja rungossa, voimme käyttää vain tyyppejä, joiden arvoja voidaan järjestää. Vertailujen mahdollistamiseksi standardikirjasto tarjoaa std::cmp::PartialOrd-traitin, jonka voit toteuttaa tyypeille (katso lisätietoja liitteestä C). Korjataksemme listauksen 10-5 voimme seurata ohjetekstin ehdotusta ja rajoittaa tyypit, jotka kelpaavat T:lle, vain niihin, jotka toteuttavat PartialOrd-traitin. Listaus kääntyy silloin, koska standardikirjasto toteuttaa PartialOrd-traitin sekä i32- että char-tyypeille.

Struct-määrittelyissä

Voimme myös määritellä structeja käyttämään geneeristä tyyppiparametria yhdessä tai useammassa kentässä <>-syntaksilla. Listaus 10-6 määrittelee Point<T>-structin, joka säilyttää x- ja y-koordinaattiarvot minkä tyyppisinä tahansa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-06/src/main.rs}}
}
Listing 10-6: Point<T>-struct, joka säilyttää tyypin T arvot x ja y

Syntaksi geneeristen tyyppien käyttöön struct-määrittelyissä on samanlainen kuin funktiomäärittelyissä. Ensin ilmoitamme tyyppiparametrin nimen kulmasulkeissa heti structin nimen jälkeen. Sitten käytämme geneeristä tyyppiä struct-määrittelyssä siinä kohdassa, missä muuten määrittelisimme konkreettiset tietotyypit.

Huomaa, että koska olemme käyttäneet vain yhtä geneeristä tyyppiä määritellessämme Point<T>:n, tämä määrittely sanoo, että Point<T>-struct on geneerinen jonkin tyypin T suhteen ja kentät x ja y ovat molemmat samaa tyyppiä, mikä se tyyppi sitten onkaan. Jos luomme Point<T>-instanssin, jonka arvot ovat eri tyyppejä, kuten listauksessa 10-7, koodimme ei käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-07/src/main.rs}}
Listing 10-7: Kenttien x ja y täytyy olla samaa tyyppiä, koska molemmat käyttävät samaa geneeristä tietotyyppiä T.

Tässä esimerkissä, kun annamme kokonaislukuarvon 5 muuttujalle x, kerromme kääntäjälle, että geneerinen tyyppi T on kokonaisluku tässä Point<T>- instanssissa. Kun määrittelemme 4.0 muuttujalle y, jonka olemme määritelleet samaan tyyppiin kuin x, saamme tyyppivirheen, joka näyttää tältä:

{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-07/output.txt}}

Määritelläksemme Point-structin, jossa x ja y ovat molemmat geneerisiä mutta voivat olla eri tyyppejä, voimme käyttää useita geneerisiä tyyppiparametreja. Esimerkiksi listauksessa 10-8 muutamme Point-structin määrittelyn geneeriseksi tyypeille T ja U, jossa x on tyyppiä T ja y on tyyppiä U.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-08/src/main.rs}}
}
Listing 10-8: Point<T, U> geneerinen kahden tyypin suhteen, jotta x ja y voivat olla eri tyyppisiä arvoja

Nyt kaikki näytetyt Point-instanssit ovat sallittuja! Voit käyttää määrittelyssä niin monta geneeristä tyyppiparametria kuin haluat, mutta useamman kuin muutaman käyttö tekee koodista vaikealukuista. Jos huomaat tarvitsevasi paljon geneerisiä tyyppejä koodissasi, se voi viitata siihen, että koodisi kaipaa uudelleenjärjestelyä pienempiin osiin.

Enum-määrittelyissä

Kuten structeissa, voimme määritellä enum-arvoja säilyttämään geneerisiä tietotyyppejä variantteihinsa. Katsotaan vielä kerran Option<T>-enum-arvoa, jonka standardikirjasto tarjoaa ja jota käytimme luvussa 6:

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

Tämän määrittelyn pitäisi nyt olla selkeämpi. Kuten näet, Option<T>-enum on geneerinen tyypin T suhteen ja sillä on kaksi varianttia: Some, joka säilyttää yhden arvon tyypistä T, ja variantti None, joka ei säilytä mitään arvoa. Käyttämällä Option<T>-enum-arvoa voimme ilmaista abstraktin käsitteen valinnaisesta arvosta, ja koska Option<T> on geneerinen, voimme käyttää tätä abstraktiota riippumatta valinnaisen arvon tyypistä.

Enum-arvot voivat käyttää myös useita geneerisiä tyyppejä. Result-enum-arvon määrittely, jota käytimme luvussa 9, on yksi esimerkki:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Result-enum on geneerinen kahden tyypin, T ja E, suhteen ja sillä on kaksi varianttia: Ok, joka säilyttää arvon tyypistä T, ja Err, joka säilyttää arvon tyypistä E. Tämä määrittely tekee Result-enum-arvon käytöstä kätevää missä tahansa, missä operaatio voi onnistua (palauttaa arvon jonkin tyypin T) tai epäonnistua (palauttaa virheen jonkin tyypin E). Tätä käytimme avatessamme tiedoston listauksessa 9-3, jossa T täytettiin tyypillä std::fs::File, kun tiedosto avattiin onnistuneesti, ja E täytettiin tyypillä std::io::Error, kun tiedoston avaamisessa oli ongelmia.

Kun tunnistat koodissasi tilanteita, joissa on useita struct- tai enum- määrittelyjä, jotka eroavat vain säilyttämiensä arvojen tyypeistä, voit välttää toiston käyttämällä geneerisiä tyyppejä.

Metodimäärittelyissä

Voimme toteuttaa metodeja structeille ja enum-arvoille (kuten teimme luvussa 5) ja käyttää geneerisiä tyyppejä niiden määrittelyissäkin. Listaus 10-9 näyttää Point<T>-structin, jonka määrittelimme listauksessa 10-6, ja siihen toteutetun x-nimisen metodin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-09/src/main.rs}}
}
Listing 10-9: Metodin x toteuttaminen Point<T>-structille, joka palauttaa viittauksen kentän x arvoon tyypistä T

Tässä olemme määritelleet Point<T>-structille metodin nimeltä x, joka palauttaa viittauksen kentän x dataan.

Huomaa, että meidän täytyy ilmoittaa T heti impl-avainsanan jälkeen, jotta voimme käyttää T:tä määrittääksemme, että toteutamme metodeja tyypille Point<T>. Ilmoittamalla T:n geneeriseksi tyypiksi impl-avainsanan jälkeen Rust tunnistaa, että kulmasulkeissa Point-tyypin jälkeen oleva tyyppi on geneerinen tyyppi eikä konkreettinen tyyppi. Olisimme voineet valita erilaisen nimen tälle geneeriselle parametrille kuin struct-määrittelyssä ilmoitettu geneerinen parametri, mutta saman nimen käyttö on käytäntö. Jos kirjoitat metodin impl-lohkon sisällä, joka ilmoittaa geneerisen tyypin, se metodi määritellään minkä tahansa tyypin instanssille, riippumatta siitä, mikä konkreettinen tyyppi korvaa geneerisen tyypin.

Voimme myös määrittää rajoituksia geneerisille tyypeille määritellessämme metodeja tyypille. Voisimme esimerkiksi toteuttaa metodeja vain Point<f32>- instansseille eikä Point<T>-instansseille millä tahansa geneerisellä tyypillä. Listauksessa 10-10 käytämme konkreettista tyyppiä f32, mikä tarkoittaa, että emme ilmoita mitään tyyppejä impl-avainsanan jälkeen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-10/src/main.rs:here}}
}
Listing 10-10: impl-lohko, joka koskee vain structia, jonka geneerisen tyyppiparametrin T konkreettinen tyyppi on tietty

Tämä koodi tarkoittaa, että tyypillä Point<f32> on distance_from_origin- metodi; muilla Point<T>-instansseilla, joissa T ei ole tyyppiä f32, ei ole tätä metodia määriteltynä. Metodi mittaa, kuinka kaukana pisteemme on pisteestä koordinaateissa (0.0, 0.0), ja käyttää matemaattisia operaatioita, jotka ovat käytettävissä vain liukulukutyypeille.

Struct-määrittelyssä olevat geneeriset tyyppiparametrit eivät aina ole samoja kuin saman structin metodien allekirjoituksissa käytetyt. Listaus 10-11 käyttää geneerisiä tyyppejä X1 ja Y1 Point-structille ja X2 ja Y2 mixup-metodin allekirjoituksessa selkeyttääkseen esimerkkiä. Metodi luo uuden Point-instanssin, jossa x-arvo tulee self-Point-instanssista (tyyppiä X1) ja y-arvo annetusta Point-instanssista (tyyppiä Y2).

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-11/src/main.rs}}
}
Listing 10-11: Metodi, joka käyttää geneerisiä tyyppejä, jotka eroavat structin määrittelystä

Funktiossa main olemme määritelleet Point-instanssin, jossa x on i32 (arvolla 5) ja y on f64 (arvolla 10.4). Muuttuja p2 on Point- struct, jossa x on merkkijonoviipale (arvolla "Hello") ja y on char (arvolla c). Kutsumalla mixup-metodia p1:llä argumenttina p2 saamme p3:n, jossa x on i32, koska x tuli p1:stä. Muuttujassa p3 y on char, koska y tuli p2:sta. println!-makrokutsu tulostaa p3.x = 5, p3.y = c.

Tämän esimerkin tarkoitus on demonstroida tilanne, jossa jotkut geneeriset parametrit ilmoitetaan impl-avainsanalla ja jotkut metodin määrittelyssä. Tässä geneeriset parametrit X1 ja Y1 ilmoitetaan impl-avainsanan jälkeen, koska ne liittyvät struct-määrittelyyn. Geneeriset parametrit X2 ja Y2 ilmoitetaan fn mixup-kohdan jälkeen, koska ne ovat merkityksellisiä vain metodille.

Geneerisiä tyyppejä käyttävän koodin suorituskyky

Saatat miettiä, aiheutuuko geneeristen tyyppiparametrien käytöstä ajonaikainen kustannus. Hyvä uutinen on, että geneeristen tyyppien käyttö ei hidasta ohjelmaasi verrattuna konkreettisiin tyyppeihin.

Rust saavuttaa tämän suorittamalla monomorfisoinnin (monomorphization) geneeristä koodista käännösaikana. Monomorfisointi on prosessi, jossa geneerinen koodi muutetaan tiettyyn koodiin täyttämällä konkreettiset tyypit, joita käytetään käännöksen yhteydessä. Tässä prosessissa kääntäjä tekee vastakkaisen kuin vaiheet, joilla loimme geneerisen funktion listauksessa 10-5: Kääntäjä tarkastaa kaikki kohdat, joissa geneeristä koodia kutsutaan, ja generoi koodin konkreettisille tyypeille, joilla geneeristä koodia kutsutaan.

Katsotaan, miten tämä toimii käyttämällä standardikirjaston geneeristä Option<T>-enum-arvoa:

#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

Kun Rust kääntää tämän koodin, se suorittaa monomorfisoinnin. Tämän prosessin aikana kääntäjä lukee arvot, joita on käytetty Option<T>-instansseissa, ja tunnistaa kaksi Option<T>-tyyppiä: toinen on i32 ja toinen f64. Näin se laajentaa Option<T>:n geneerisen määrittelyn kahteen i32- ja f64- erikoistuneeseen määrittelyyn korvaten geneerisen määrittelyn tiettyillä määrittelyillä.

Monomorfisoidun version koodi näyttää suunnilleen seuraavalta (kääntäjä käyttää eri nimiä kuin mitä käytämme tässä havainnollistukseen):

Filename: src/main.rs
enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Geneerinen Option<T> korvataan kääntäjän luomilla tiettyillä määrittelyillä. Koska Rust kääntää geneerisen koodin koodiksi, joka määrittää tyypin jokaisessa instanssissa, emme maksa ajonaikaista kustannusta geneeristen tyyppien käytöstä. Kun koodi suoritetaan, se toimii aivan kuten jos olisimme monistaneet jokaisen määrittelyn käsin. Monomorfisointiprosessi tekee Rustin geneerisistä tyypeistä erittäin tehokkaita ajonaikana.

Traitit: jaetun käyttäytymisen määrittäminen

Jaetun käyttäytymisen määrittely traittejen avulla

Trait määrittelee toiminnallisuuden, joka tietyllä tyypillä on ja jonka se voi jakaa muiden tyyppien kanssa. Voimme käyttää traitteja määrittelemään jaettua käyttäytymistä abstraktisti. Voimme käyttää trait-rajoja (trait bounds) määrittääksemme, että geneerinen tyyppi voi olla mikä tahansa tyyppi, jolla on tiettyä käyttäytymistä.

Huom: Traitit muistuttavat ominaisuutta, jota muissa kielissä usein kutsutaan rajapinnoiksi (interfaces), vaikka niissä on eroja.

Traitin määrittely

Tyypin käyttäytyminen koostuu metodeista, joita voimme kutsua kyseiselle tyypille. Eri tyypit jakavat saman käyttäytymisen, jos voimme kutsua samoja metodeja kaikille näille tyypeille. Trait-määrittelyt ovat tapa ryhmittää metodien allekirjoituksia yhteen määrittelemään joukko käyttäytymisiä, joita tarvitaan jonkin tarkoituksen saavuttamiseksi.

Esimerkiksi sanotaan, että meillä on useita structeja, jotka säilyttävät erilaisia tekstejä ja määriä: NewsArticle-struct, joka säilyttää uutisjutun tietyssä paikassa, ja SocialPost, jossa voi olla enintään 280 merkkiä sekä metatietoa siitä, oliko kyseessä uusi julkaisu, uudelleenjulkaisu vai vastaus toiseen julkaisuun.

Haluamme tehdä aggregator-nimisen mediakoostajakirjaston, joka voi näyttää yhteenvetoja tiedoista, jotka voivat olla NewsArticle- tai SocialPost- instanssissa. Tarvitsemme yhteenvedon kustakin tyypistä ja pyydämme sitä kutsumalla summarize-metodia instanssilla. Listaus 10-12 näyttää julkisen Summary-traitin määrittelyn, joka ilmaisee tämän käyttäytymisen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-12/src/lib.rs}}
Listing 10-12: Summary-trait, joka koostuu summarize-metodin tarjoamasta käyttäytymisestä

Tässä ilmoitamme traitin trait-avainsanalla ja sitten traitin nimen, joka tässä tapauksessa on Summary. Ilmoitamme traitin myös pub-määritteellä, jotta tästä cratesta riippuvat cratet voivat käyttää tätä traitia myös, kuten näemme muutamassa esimerkissä. Aaltosulkeiden sisällä ilmoitamme metodien allekirjoitukset, jotka kuvaavat tämän traitin toteuttavien tyyppien käyttäytymistä, joka tässä tapauksessa on fn summarize(&self) -> String.

Metodin allekirjoituksen jälkeen käytämme puolipistettä aaltosulkeiden sijaan. Jokaisen tämän traitin toteuttavan tyypin täytyy tarjota oma mukautettu käyttäytymänsä metodin rungolle. Kääntäjä varmistaa, että jokaisella tyypillä, jolla on Summary-trait, on metodi summarize määriteltynä täsmälleen tällä allekirjoituksella.

Traitilla voi olla useita metodeja rungossaan: Metodien allekirjoitukset luetellaan yksi per rivi, ja jokainen rivi päättyy puolipisteeseen.

Traitin toteuttaminen tyypille

Nyt kun olemme määritelleet Summary-traitin metodien halutut allekirjoitukset, voimme toteuttaa sen mediakoostajamme tyypeille. Listaus 10-13 näyttää Summary-traitin toteutuksen NewsArticle-structille, joka käyttää otsikkoa, kirjoittajaa ja sijaintia luodakseen summarize-metodin palautusarvon. SocialPost-structille määrittelemme summarize-metodin käyttäjänimen jälkeen koko julkaisun tekstin olettaen, että julkaisun sisältö on jo rajoitettu 280 merkkiin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-13/src/lib.rs:here}}
Listing 10-13: Summary-traitin toteuttaminen tyypeille NewsArticle ja SocialPost

Traitin toteuttaminen tyypille on samanlaista kuin tavallisten metodien toteuttaminen. Ero on siinä, että impl-avainsanan jälkeen laitamme toteutettavan traitin nimen, sitten for-avainsanan ja sitten tyypin nimen, jolle haluamme toteuttaa traitin. impl-lohkon sisällä laitamme metodien allekirjoitukset, jotka trait-määrittely on määritellyt. Sen sijaan, että lisäisimme puolipisteen jokaisen allekirjoituksen jälkeen, käytämme aaltosulkeita ja täytämme metodin rungon traitin metodien haluamalla käyttäytymisellä kyseiselle tyypille.

Nyt kun kirjasto on toteuttanut Summary-traitin NewsArticle- ja SocialPost-tyypeille, craten käyttäjät voivat kutsua trait-metodeja NewsArticle- ja SocialPost-instansseilla samalla tavalla kuin kutsumme tavallisia metodeja. Ainoa ero on, että käyttäjän täytyy tuoda trait näkyviin tyyppejen ohella. Tässä on esimerkki siitä, miten binääricrate voisi käyttää aggregator-kirjastocrateamme:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-01-calling-trait-method/src/main.rs}}

Tämä koodi tulostaa 1 new post: horse_ebooks: of course, as you probably already know, people.

Muut aggregator-cratesta riippuvat cratet voivat myös tuoda Summary- traitin näkyviin toteuttaakseen Summary-traitin omille tyypeilleen. Yksi rajoitus on huomionarvoista: voimme toteuttaa traitin tyypille vain, jos joko trait tai tyyppi, tai molemmat, ovat paikallisia crateamme. Esimerkiksi voimme toteuttaa standardikirjaston traitteja kuten Display mukautetulle tyypille kuten SocialPost osana aggregator-cratemme toiminnallisuutta, koska tyyppi SocialPost on paikallinen aggregator-cratemme. Voimme myös toteuttaa Summary-traitin Vec<T>:lle aggregator-cratemme, koska trait Summary on paikallinen aggregator-cratemme.

Emme voi kuitenkaan toteuttaa ulkoisia traitteja ulkoisille tyypeille. Esimerkiksi emme voi toteuttaa Display-traitia Vec<T>:lle aggregator-cratemme sisällä, koska Display ja Vec<T> on molemmat määritelty standardikirjastossa eivätkä ole paikallisia aggregator-cratemme. Tämä rajoitus on osa ominaisuutta nimeltä koherenssi (coherence), ja tarkemmin orvokkosääntöä (orphan rule), joka on nimetty niin, koska ylätyyppi ei ole läsnä. Tämä sääntö varmistaa, etteivät muiden ihmisten koodit voi rikkoa sinun koodiasi ja päinvastoin. Ilman sääntöä kaksi cratea voisi toteuttaa saman traitin samalle tyypille, eikä Rust tietäisi, mitä toteutusta käyttää.

Oletustoteutusten käyttö

Joskus on hyödyllistä, että traitin joillakin tai kaikilla metodeilla on oletuskäyttäytymistä sen sijaan, että vaadittaisiin toteutuksia kaikille metodeille jokaiselle tyypille. Sitten, kun toteutamme traitin tietylle tyypille, voimme säilyttää tai ohittaa kunkin metodin oletuskäyttäytymisen.

Listauksessa 10-14 määrittelemme oletusmerkkijonon Summary-traitin summarize-metodille sen sijaan, että määrittelisimme vain metodin allekirjoituksen, kuten teimme listauksessa 10-12.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-14/src/lib.rs:here}}
Listing 10-14: Summary-traitin määrittely summarize-metodin oletustoteutuksella

Käyttääksemme oletustoteutusta NewsArticle-instanssien yhteenvetoon määrittelemme tyhjän impl-lohkon impl Summary for NewsArticle {}.

Vaikka emme enää määrittele summarize-metodia suoraan NewsArticle-tyypille, olemme tarjonneet oletustoteutuksen ja määritelleet, että NewsArticle toteuttaa Summary-traitin. Näin voimme silti kutsua summarize-metodia NewsArticle-instanssilla, näin:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-02-calling-default-impl/src/main.rs:here}}

Tämä koodi tulostaa New article available! (Read more...).

Oletustoteutuksen luominen ei vaadi meitä muuttamaan mitään Summary-traitin toteutuksesta SocialPost-tyypille listauksessa 10-13. Syy on, että syntaksi oletustoteutuksen ohittamiseen on sama kuin trait-metodin toteuttamiseen, jolla ei ole oletustoteutusta.

Oletustoteutukset voivat kutsua muita metodeja samassa traitissa, vaikka näillä muilla metodeilla ei olisikaan oletustoteutusta. Näin trait voi tarjota paljon hyödyllistä toiminnallisuutta ja vaatia toteuttajilta vain pienen osan määrittelyä. Esimerkiksi voisimme määritellä Summary-traitille summarize_author-metodin, jonka toteutus on pakollinen, ja sitten määritellä summarize-metodin, jolla on oletustoteutus, joka kutsuu summarize_author- metodia:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:here}}

Käyttääksemme tätä Summary-traitin versiota meidän täytyy määritellä vain summarize_author, kun toteutamme traitin tyypille:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:impl}}

Kun olemme määritelleet summarize_author-metodin, voimme kutsua summarize- metodia SocialPost-structin instansseilla, ja summarize-metodin oletustoteutus kutsuu tarjoamaamme summarize_author-määrittelyä. Koska olemme toteuttaneet summarize_author-metodin, Summary-trait on antanut meille summarize-metodin käyttäytymisen ilman, että meidän tarvitsee kirjoittaa enempää koodia. Tältä se näyttää:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/main.rs:here}}

Tämä koodi tulostaa 1 new post: (Read more from @horse_ebooks...).

Huomaa, ettei ole mahdollista kutsua oletustoteutusta saman metodin ohittavasta toteutuksesta.

Traittien käyttö parametreina

Nyt kun tiedät, miten traitteja määritellään ja toteutetaan, voimme tutkia, miten traitteja käytetään määrittelemään funktioita, jotka hyväksyvät monia eri tyyppejä. Käytämme Summary-traitia, jonka toteutimme NewsArticle- ja SocialPost-tyypeille listauksessa 10-13, määritelläksemme notify-funktion, joka kutsuu summarize-metodia item-parametrillaan, joka on jokin tyyppi, joka toteuttaa Summary-traitin. Teemme tämän käyttämällä impl Trait- syntaksia, näin:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-04-traits-as-parameters/src/lib.rs:here}}

Sen sijaan, että item-parametrilla olisi konkreettinen tyyppi, määrittelemme impl-avainsanan ja traitin nimen. Tämä parametri hyväksyy minkä tahansa tyypin, joka toteuttaa määritellyn traitin. notify-funktion rungossa voimme kutsua mitä tahansa item-metodeja, jotka tulevat Summary-traitista, kuten summarize. Voimme kutsua notify-funktiota ja antaa sille minkä tahansa NewsArticle- tai SocialPost-instanssin. Koodi, joka kutsuu funktiota millä tahansa muulla tyypillä, kuten String tai i32, ei käänny, koska nämä tyypit eivät toteuta Summary-traitia.

Trait-rajasyntaksi

impl Trait -syntaksi toimii suoraviivaisissa tapauksissa, mutta se on itse asiassa syntaktista sokeria pidemmälle muodolle, joka tunnetaan nimellä trait-raja (trait bound); se näyttää tältä:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Tämä pidempi muoto on vastaava kuin edellisen osion esimerkki, mutta sanallisempi. Sijoitamme trait-rajat geneerisen tyyppiparametrin ilmoituksen jälkeen kaksoispisteen jälkeen kulmasulkeisiin.

impl Trait -syntaksi on kätevä ja tekee koodista tiiviimpää yksinkertaisissa tapauksissa, kun taas täydellisempi trait-rajasyntaksi voi ilmaista monimutkaisempaa muissa tapauksissa. Esimerkiksi voimme olla kaksi parametria, jotka toteuttavat Summary-traitin. Tämä impl Trait -syntaksilla näyttää tältä:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

impl Trait -syntaksin käyttö on sopivaa, jos haluamme tämän funktion sallivan item1:n ja item2:n olevan eri tyyppejä (kunhan molemmat tyypit toteuttavat Summary-traitin). Jos haluamme kuitenkin pakottaa molemmat parametrit olemaan samaa tyyppiä, meidän täytyy käyttää trait-rajaa, näin:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

Geneerinen tyyppi T, joka on määritelty item1- ja item2-parametrien tyypiksi, rajoittaa funktion siten, että argumenttina annetun arvon konkreettisen tyypin täytyy olla sama item1:lle ja item2:lle.

Useat trait-rajat +-syntaksilla

Voimme myös määrittää useamman kuin yhden trait-rajan. Sanotaan, että haluamme notify-funktion käyttävän näyttömuotoilua sekä summarize-metodia item-parametrilla: Määrittelemme notify-määrittelyssä, että item täytyy toteuttaa sekä Display että Summary. Voimme tehdä näin käyttämällä +- syntaksia:

pub fn notify(item: &(impl Summary + Display)) {

+-syntaksi on kelvollinen myös geneeristen tyyppien trait-rajoissa:

pub fn notify<T: Summary + Display>(item: &T) {

Kun kaksi trait-rajaa on määritelty, notify-funktion runko voi kutsua summarize-metodia ja käyttää {}-muotoilua item-parametrille.

Selkeämmät trait-rajat where-lausekkeilla

Liian monien trait-rajojen käytöllä on haittapuolensa. Jokaisella geneerisellä tyypillä on omat trait-rajansa, joten funktioilla, joilla on useita geneerisiä tyyppiparametreja, voi olla paljon trait-rajatietoa funktion nimen ja parametrilistan välissä, mikä tekee funktion allekirjoituksesta vaikealukuisen. Tästä syystä Rustissa on vaihtoehtoinen syntaksi trait-rajojen määrittelyyn where-lausekkeessa funktion allekirjoituksen jälkeen. Sen sijaan, että kirjoittaisimme tämän:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

voimme käyttää where-lauseketta, näin:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-where-clause/src/lib.rs:here}}

Tämän funktion allekirjoitus on vähemmän sekava: Funktion nimi, parametrilista ja palautustyyppi ovat lähellä toisiaan, samoin kuin funktiossa, jossa ei ole paljon trait-rajoja.

Traitin toteuttavien tyyppien palauttaminen

Voimme myös käyttää impl Trait -syntaksia palautuspaikassa palauttaaksemme jonkin traitin toteuttavan tyypin arvon, kuten tässä:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-05-returning-impl-trait/src/lib.rs:here}}

Käyttämällä impl Summary palautustyypinä määrittelemme, että returns_summarizable-funktio palauttaa jonkin Summary-traitin toteuttavan tyypin nimeämättä konkreettista tyyppiä. Tässä tapauksessa returns_summarizable palauttaa SocialPost-instanssin, mutta tätä funktiota kutsuvan koodin ei tarvitse tietää sitä.

Mahdollisuus määrittää palautustyyppi vain sen toteuttaman traitin perusteella on erityisen hyödyllinen sulkujen ja iteraattorien yhteydessä, joita käsittelemme luvussa 13. Sulut ja iteraattorit luovat tyyppejä, jotka vain kääntäjä tuntee, tai tyyppejä, joiden määrittely on hyvin pitkä. impl Trait -syntaksi antaa sinun määrittää tiiviisti, että funktio palauttaa jonkin Iterator-traitin toteuttavan tyypin ilman, että sinun tarvitsee kirjoittaa hyvin pitkää tyyppiä.

Voit kuitenkin käyttää impl Trait -syntaksia vain, jos palautat yhden tyypin. Esimerkiksi tämä koodi, joka palauttaa joko NewsArticle- tai SocialPost- instanssin palautustyypin ollessa impl Summary, ei toimisi:

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-06-impl-trait-returns-one-type/src/lib.rs:here}}

Joko NewsArticle- tai SocialPost-instanssin palauttaminen ei ole sallittua impl Trait -syntaksin kääntäjässä toteutettujen rajoitusten vuoksi. Käsittelemme, miten kirjoitamme funktion tällä käyttäytymisellä luvun 18 osiossa ”Trait- olioiden käyttö jaetun käyttäytymisen abstrahointiin”.

Metodien ehdollinen toteuttaminen trait-rajojen avulla

Käyttämällä trait-rajaa impl-lohkossa, joka käyttää geneerisiä tyyppiparametreja, voimme toteuttaa metodeja ehdollisesti tyypeille, jotka toteuttavat määritellyt traitit. Esimerkiksi tyyppi Pair<T> listauksessa 10-15 toteuttaa aina new-funktion palauttaakseen uuden Pair<T>-instanssin (muista luvun 5 ”Metodisyntaksi” -osiosta, että Self on tyyppialias impl-lohkon tyypille, joka tässä tapauksessa on Pair<T>). Mutta seuraavassa impl-lohkossa Pair<T> toteuttaa cmp_display- metodin vain, jos sen sisäinen tyyppi T toteuttaa PartialOrd-traitin, joka mahdollistaa vertailun, ja Display-traitin, joka mahdollistaa tulostamisen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-15/src/lib.rs}}
Listing 10-15: Metodien ehdollinen toteuttaminen geneeriselle tyypille trait-rajojen mukaan

Voimme myös toteuttaa traitin ehdollisesti mille tahansa tyypille, joka toteuttaa toisen traitin. Traitin toteutukset mille tahansa tyypille, joka täyttää trait-rajat, kutsutaan peittoimplementaatioiksi (blanket implementations), ja niitä käytetään laajasti Rustin standardikirjastossa. Esimerkiksi standardikirjasto toteuttaa ToString-traitin mille tahansa tyypille, joka toteuttaa Display-traitin. Standardikirjaston impl-lohko näyttää suunnilleen tältä:

impl<T: Display> ToString for T {
    // --snip--
}

Koska standardikirjastossa on tämä peittoimplementaatio, voimme kutsua ToString-traitin määrittelemää to_string-metodia millä tahansa tyypillä, joka toteuttaa Display-traitin. Esimerkiksi voimme muuttaa kokonaisluvut vastaaviksi String-arvoiksi näin, koska kokonaisluvut toteuttavat Display-traitin:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Peittoimplementaatiot näkyvät traitin dokumentaatiossa ”Implementors”-osiossa.

Traitit ja trait-rajat antavat meille mahdollisuuden kirjoittaa koodia, joka käyttää geneerisiä tyyppiparametreja vähentääkseen toistoa, mutta myös määrittää kääntäjälle, että haluamme geneerisen tyypin omaavan tiettyä käyttäytymistä. Kääntäjä voi sitten käyttää trait-rajatietoa tarkistaakseen, että kaikki koodissamme käytetyt konkreettiset tyypit tarjoavat oikean käyttäytymisen. Dynaamisesti tyypitetyissä kielissä saisimme virheen ajonaikana, jos kutsuisimme metodia tyypille, joka ei määrittele metodia. Mutta Rust siirtää nämä virheet käännösaikaan, joten meidän täytyy korjata ongelmat ennen kuin koodimme edes pystyy suorittumaan. Lisäksi emme tarvitse kirjoittaa koodia, joka tarkistaa käyttäytymistä ajonaikana, koska olemme jo tarkistaneet käännösaikana. Näin parannamme suorituskykyä luopumatta geneeristen tyyppien joustavuudesta.

Viitteiden vahvistaminen eliniillä

Viittausten validointi elinikien avulla

Eliniät (lifetimes) ovat toinen geneeristen tyyppien laji, jota olemme jo käyttäneet. Sen sijaan, että varmistaisimme tyypin käyttäytyvän haluamallamme tavalla, eliniät varmistavat, että viittaukset ovat kelvollisia niin kauan kuin tarvitsemme niitä.

Yksi yksityiskohta, jota emme käsitelleet luvun 4 osiossa ”Viittaukset ja lainaaminen”, on se, että jokaisella Rustin viittauksella on elinikä, joka on alue, jolla viittaus on kelvollinen. Useimmiten eliniät ovat implisiittisiä ja pääteltyjä, aivan kuten useimmiten tyypit ovat pääteltyjä. Meidän täytyy ilmoittaa tyypit vain, kun useita tyyppejä on mahdollista. Vastaavasti meidän täytyy merkitä eliniät, kun viittausten eliniöillä voi olla useita eri suhteita. Rust vaatii meitä merkitsemään suhteet geneeristen elinikäparametrien avulla varmistaakseen, että ajonaikana käytetyt viittaukset ovat varmasti kelvollisia.

Elinikien merkitseminen ei ole edes käsite, jota useimmissa muissa ohjelmointikielissä on, joten tämä tuntuu tutustumattomalta. Vaikka emme käsittele eliniä kokonaisuudessaan tässä luvussa, käsittelemme yleisiä tapoja, joilla voit kohdata elinikäsyntaksia, jotta voit tottua käsitteeseen.

Riippuvat viittaukset

Elinikien pääasiallinen tarkoitus on estää riippuvia viittauksia (dangling references), jotka, jos niiden olemassaolo sallittaisiin, saisivat ohjelman viittaamaan muihin tietoihin kuin niihin, joihin sen on tarkoitus viitata. Harkitse listauksen 10-16 ohjelmaa, jossa on ulompi ja sisempi näkyvyysalue.

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/src/main.rs}}
Listing 10-16: Yritys käyttää viittausta, jonka arvo on poistunut näkyvyysalueelta

Huom: Listausten 10-16, 10-17 ja 10-23 esimerkit ilmoittavat muuttujia antamatta niille alkuarvoa, joten muuttujan nimi on olemassa ulommalla näkyvyysalueella. Ensi silmäyksellä tämä saattaa vaikuttaa ristiriitaiselta Rustin null-arvojen puuttumisen kanssa. Jos kuitenkin yritämme käyttää muuttujaa ennen arvon antamista, saamme käännösvirheen, mikä osoittaa, että Rust todellakaan ei salli null-arvoja.

Ulompi näkyvyysalue ilmoittaa muuttujan nimeltä r ilman alkuarvoa, ja sisempi näkyvyysalue ilmoittaa muuttujan nimeltä x alkuarvolla 5. Sisemmällä näkyvyysalueella yritämme asettaa r:n arvoksi viittauksen x:ään. Sitten sisempi näkyvyysalue päättyy, ja yritämme tulostaa r:n arvon. Tämä koodi ei käänny, koska arvo, johon r viittaa, on poistunut näkyvyysalueelta ennen kuin yritämme käyttää sitä. Tässä on virheilmoitus:

{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/output.txt}}

Virheilmoitus sanoo, että muuttuja x ”does not live long enough”. Syy on, että x on näkyvyysalueen ulkopuolella, kun sisempi näkyvyysalue päättyy rivillä 7. Mutta r on silti kelvollinen ulommalla näkyvyysalueella; koska sen näkyvyysalue on suurempi, sanomme, että se ”elää pidempään”. Jos Rust sallisi tämän koodin toimia, r viittaisi muistiin, joka vapautettiin, kun x poistui näkyvyysalueelta, eikä mikään, mitä yrittäisimme tehdä r:llä, toimisi oikein. Miten Rust määrittää, että tämä koodi on virheellinen? Se käyttää lainauskontrolleria.

Lainauskontrolleri

Rustin kääntäjällä on lainauskontrolleri (borrow checker), joka vertaa näkyvyysalueita määrittääkseen, ovatko kaikki lainaukset kelvollisia. Listaus 10-17 näyttää saman koodin kuin listaus 10-16, mutta merkinnöillä, jotka näyttävät muuttujien eliniät.

{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-17/src/main.rs}}
Listing 10-17: Muuttujien r ja x eliniöiden merkinnät, nimiltään 'a ja 'b

Tässä olemme merkinneet r:n eliniän 'a:lla ja x:n eliniän 'b:llä. Kuten näet, sisempi 'b-lohko on paljon pienempi kuin ulompi 'a-elinikälohko. Käännösaikana Rust vertaa kahden eliniän kokoa ja näkee, että r:llä on elinikä 'a, mutta se viittaa muistiin, jonka elinikä on 'b. Ohjelma hylätään, koska 'b on lyhyempi kuin 'a: Viittauksen kohteella ei ole yhtä pitkää elinikää kuin viittauksella.

Listaus 10-18 korjaa koodin niin, ettei siinä ole riippuvaa viittausta, ja se kääntyy ilman virheitä.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-18/src/main.rs}}
}
Listing 10-18: Kelvollinen viittaus, koska datalla on pidempi elinikä kuin viittauksella

Tässä x:llä on elinikä 'b, joka tässä tapauksessa on suurempi kuin 'a. Tämä tarkoittaa, että r voi viitata x:ään, koska Rust tietää, että r:n viittaus on aina kelvollinen niin kauan kuin x on kelvollinen.

Nyt kun tiedät, missä viittausten eliniät ovat ja miten Rust analysoi eliniitä varmistaakseen, että viittaukset ovat aina kelvollisia, tutkitaan geneerisiä eliniä funktioiden parametreissa ja palautusarvoissa.

Geneeriset eliniät funktioissa

Kirjoitamme funktion, joka palauttaa pidemmän kahdesta merkkijonoviipaleesta. Tämä funktio ottaa kaksi merkkijonoviipaletta ja palauttaa yhden merkkijonoviipaleen. Kun olemme toteuttaneet longest-funktion, listauksen 10-19 koodin pitäisi tulostaa The longest string is abcd.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-19/src/main.rs}}
Listing 10-19: main-funktio, joka kutsuu longest-funktiota löytääkseen pidemmän kahdesta merkkijonoviipaleesta

Huomaa, että haluamme funktion ottavan merkkijonoviipaleita, jotka ovat viittauksia, eivät merkkijonoja, koska emme halua longest-funktion ottavan parametriensa omistajuutta. Katso lisätietoja siitä, miksi listauksen 10-19 parametrit ovat haluamiamme, luvun 4 osiosta ”Merkkijonoviipaleet parametreina”.

Jos yritämme toteuttaa longest-funktion kuten listauksessa 10-20, se ei käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/src/main.rs:here}}
Listing 10-20: longest-funktion toteutus, joka palauttaa pidemmän kahdesta merkkijonoviipaleesta mutta ei vielä käänny

Sen sijaan saamme seuraavan virheen, joka käsittelee eliniitä:

{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/output.txt}}

Ohjeteksti paljastaa, että palautustyypillä täytyy olla geneerinen elinikäparametri, koska Rust ei voi kertoa, viittaako palautettava viittaus x:ään vai y:ään. Itse asiassa emme tiedä sitäkään, koska funktion rungon if-lohko palauttaa viittauksen x:ään ja else-lohko viittauksen y:ään!

Kun määrittelemme tämän funktion, emme tiedä konkreettisia arvoja, jotka annetaan tälle funktiolle, joten emme tiedä, suoritetaanko if- vai else- tapaus. Emme myöskään tiedä viittausten konkreettisia eliniä, joten emme voi tarkastella näkyvyysalueita kuten listauksissa 10-17 ja 10-18 määrittääksemme, onko palauttamamme viittaus aina kelvollinen. Lainauskontrolleri ei voi määrittää tätäkään, koska se ei tiedä, miten x:n ja y:n eliniät liittyvät palautusarvon elinikään. Korjataksemme tämän virheen lisäämme geneerisiä elinikäparametreja, jotka määrittelevät viittausten välisen suhteen, jotta lainauskontrolleri voi suorittaa analyysinsä.

Elinikämerkintöjen syntaksi

Elinikämerkinnät eivät muuta sitä, kuinka kauan mikään viittauksista elää. Pikemminkin ne kuvaavat useiden viittausten eliniöiden välisiä suhteita vaikuttamatta eliniöihin. Aivan kuten funktiot voivat hyväksyä minkä tahansa tyypin, kun allekirjoitus määrittää geneerisen tyyppiparametrin, funktiot voivat hyväksyä viittauksia millä tahansa eliniällä määrittämällä geneerisen elinikäparametrin.

Elinikämerkinnöillä on hieman epätavallinen syntaksi: Elinikäparametrien nimien täytyy alkaa heittomerkillä (') ja ne ovat yleensä pieniä kirjaimia ja hyvin lyhyitä, kuten geneeriset tyypit. Useimmat ihmiset käyttävät nimeä 'a ensimmäiselle elinikämerkinnälle. Sijoitamme elinikäparametrien merkinnät &-merkin jälkeen viittauksessa käyttäen välilyöntiä erottamaan merkinnän viittauksen tyypistä.

Tässä on joitakin esimerkkejä — viittaus i32:een ilman elinikäparametria, viittaus i32:een, jolla on elinikäparametri nimeltä 'a, ja muuttuva viittaus i32:een, jolla on myös elinikä 'a:

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

Yksi elinikämerkintä itsessään ei merkitse paljon, koska merkinnät on tarkoitettu kertomaan Rustille, miten useiden viittausten geneeriset elinikäparametrit liittyvät toisiinsa. Tarkastellaan, miten elinikämerkinnät liittyvät toisiinsa longest-funktion yhteydessä.

Funktioiden allekirjoituksissa

Käyttääksemme elinikämerkintöjä funktioiden allekirjoituksissa meidän täytyy ilmoittaa geneeriset elinikäparametrit kulmasulkeissa funktion nimen ja parametrilistan välissä, aivan kuten teimme geneeristen tyyppiparametrien kanssa.

Haluamme allekirjoituksen ilmaisevan seuraavan rajoituksen: Palautettu viittaus on kelvollinen niin kauan kuin molemmat parametrit ovat kelvollisia. Tämä on parametrien ja palautusarvon eliniöiden välinen suhde. Nimeämme eliniän 'a:ksi ja lisäämme sen jokaiseen viittaukseen, kuten listauksessa 10-21.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-21/src/main.rs:here}}
}
Listing 10-21: longest-funktion määrittely, joka määrittää, että kaikilla allekirjoituksen viittauksilla täytyy olla sama elinikä 'a

Tämän koodin pitäisi kääntyä ja tuottaa haluamamme tuloksen, kun käytämme sitä listauksen 10-19 main-funktion kanssa.

Funktion allekirjoitus kertoo nyt Rustille, että jollekin eliniälle 'a funktio ottaa kaksi parametria, jotka molemmat ovat merkkijonoviipaleita, jotka elävät vähintään yhtä kauan kuin elinikä 'a. Funktion allekirjoitus kertoo myös Rustille, että funktiosta palautettu merkkijonoviipale elää vähintään yhtä kauan kuin elinikä 'a. Käytännössä tämä tarkoittaa, että longest-funktion palauttaman viittauksen elinikä on sama kuin funktioargumenttien viittaamien arvojen eliniöistä lyhyempi. Nämä suhteet ovat ne, joita haluamme Rustin käyttävän analysoidessaan tätä koodia.

Muista, että kun määrittelemme elinikäparametrit tässä funktion allekirjoituksessa, emme muuta annettujen tai palautettujen arvojen eliniöitä. Pikemminkin määrittelemme, että lainauskontrollerin täytyy hylätä kaikki arvot, jotka eivät noudata näitä rajoituksia. Huomaa, että longest-funktion ei tarvitse tietää tarkalleen, kuinka kauan x ja y elävät, vain että jokin näkyvyysalue voidaan korvata 'a:lla, joka tyydyttää tämän allekirjoituksen.

Kun merkitsemme eliniöitä funktioissa, merkinnät menevät funktion allekirjoitukseen, ei funktion runkoon. Elinikämerkinnät tulevat osaksi funktion sopimusta, aivan kuten allekirjoituksen tyypit. Funktioallekirjoitusten sisältäminen elinikäsopimus tarkoittaa, että Rust-kääntäjän suorittama analyysi voi olla yksinkertaisempaa. Jos on ongelma funktion merkinnöissä tai sen kutsumisessa, kääntäjän virheet voivat osoittaa koodimme osan ja rajoitukset tarkemmin. Jos sen sijaan Rust- kääntäjä tekisi enemmän päätelmiä siitä, mitä aioimme eliniöiden suhteilla, kääntäjä saattaisi pystyä osoittamaan vain koodimme käytön monia askelia ongelman syystä.

Kun annamme konkreettisia viittauksia longest-funktiolle, konkreettinen elinikä, joka korvaa 'a:n, on osa x:n näkyvyysalueesta, joka leikkaa y:n näkyvyysalueen kanssa. Toisin sanoen geneerinen elinikä 'a saa konkreettisen eliniän, joka on yhtä suuri kuin x:n ja y:n eliniöistä lyhyempi. Koska olemme merkinneet palautetun viittauksen samalla elinikäparametrilla 'a, palautettu viittaus on myös kelvollinen x:n ja y:n eliniöistä lyhyemmän pituuden ajan.

Katsotaan, miten elinikämerkinnät rajoittavat longest-funktiota antamalla viittauksia, joilla on eri konkreettiset eliniät. Listaus 10-22 on suoraviivainen esimerkki.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-22/src/main.rs:here}}
}
Listing 10-22: longest-funktion käyttö viittauksilla String-arvoihin, joilla on eri konkreettiset eliniät

Tässä esimerkissä string1 on kelvollinen ulomman näkyvyysalueen loppuun asti, string2 on kelvollinen sisemmän näkyvyysalueen loppuun asti, ja result viittaa johonkin, joka on kelvollinen sisemmän näkyvyysalueen loppuun asti. Aja tämä koodi ja näet, että lainauskontrolleri hyväksyy sen; se kääntyy ja tulostaa The longest string is long string is long.

Seuraavaksi kokeillaan esimerkkiä, joka näyttää, että result-viittauksen elinikän täytyy olla kahden argumentin lyhyempi elinikä. Siirrämme result- muuttujan ilmoituksen sisemmän näkyvyysalueen ulkopuolelle, mutta jätämme arvon result-muuttujalle antamisen sisemmän näkyvyysalueen sisälle string2:n kanssa. Sitten siirrämme result:ia käyttävän println!:n sisemmän näkyvyysalueen ulkopuolelle, sen päättymisen jälkeen. Listauksen 10-23 koodi ei käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/src/main.rs:here}}
Listing 10-23: Yritys käyttää result-muuttujaa sen jälkeen, kun string2 on poistunut näkyvyysalueelta

Kun yritämme kääntää tämän koodin, saamme tämän virheen:

{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/output.txt}}

Virhe näyttää, että jotta result olisi kelvollinen println!-lauseelle, string2:n täytyisi olla kelvollinen ulomman näkyvyysalueen loppuun asti. Rust tietää tämän, koska olemme merkinneet funktioiden parametrien ja palautusarvojen eliniät käyttämällä samaa elinikäparametria 'a.

Ihmisinä voimme katsoa tätä koodia ja nähdä, että string1 on pidempi kuin string2, ja siksi result sisältää viittauksen string1:een. Koska string1 ei ole vielä poistunut näkyvyysalueelta, viittaus string1:een on silti kelvollinen println!-lauseelle. Kääntäjä ei kuitenkaan näe, että viittaus on kelvollinen tässä tapauksessa. Olemme kertoneet Rustille, että longest- funktion palauttaman viittauksen elinikä on sama kuin annettujen viittausten eliniöistä lyhyempi. Siksi lainauskontrolleri kieltää listauksen 10-23 koodin, koska siinä voi olla virheellinen viittaus.

Kokeile suunnitella lisää kokeita, joissa vaihtelet longest-funktiolle annettujen viittausten arvoja ja eliniöitä sekä sitä, miten palautettua viittausta käytetään. Tee hypoteeseja siitä, läpäisevätkö kokeesi lainauskontrollerin ennen kääntämistä; tarkista sitten, olitko oikeassa!

Suhteet

Tapa, jolla sinun täytyy määrittää elinikäparametrit, riippuu siitä, mitä funktiosi tekee. Esimerkiksi, jos muuttaisimme longest-funktion toteutusta palauttamaan aina ensimmäisen parametrin pidemmän merkkijonoviipaleen sijaan, emme tarvitsisi määrittää elinikää y-parametrille. Seuraava koodi kääntyy:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-08-only-one-reference-with-lifetime/src/main.rs:here}}
}

Olemme määrittäneet elinikäparametrin 'a parametrille x ja palautustyypille, mutta emme parametrille y, koska y:n eliniällä ei ole mitään suhdetta x:n tai palautusarvon elinikään.

Kun palautamme viittauksen funktiosta, palautustyypin elinikäparametrin täytyy vastata jonkin parametrin elinikäparametria. Jos palautettu viittaus ei viittaa mihinkään parametreista, sen täytyy viitata tässä funktiossa luotuun arvoon. Tämä olisi kuitenkin riippuva viittaus, koska arvo poistuu näkyvyysalueelta funktion lopussa. Harkitse tätä yritystä toteuttaa longest- funktio, joka ei käänny:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-09-unrelated-lifetime/src/main.rs:here}}

Tässä, vaikka olemme määrittäneet elinikäparametrin 'a palautustyypille, tämä toteutus ei käänny, koska palautusarvon elinikä ei liity parametrien eliniöihin ollenkaan. Tässä on virheilmoitus, jonka saamme:

{{#include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-09-unrelated-lifetime/output.txt}}

Ongelma on, että result poistuu näkyvyysalueelta ja siivotaan longest- funktion lopussa. Yritämme myös palauttaa viittauksen result:iin funktiosta. Emme voi määrittää elinikäparametreja, jotka muuttaisivat riippuvaa viittausta, eikä Rust anna meidän luoda riippuvaa viittausta. Tässä tapauksessa paras korjaus olisi palauttaa omistettu tietotyyppi viittauksen sijaan, jolloin kutsuva funktio on vastuussa arvon siivoamisesta.

Lopulta elinikäsyntaksi koskee eri parametrien ja funktioiden palautusarvojen eliniöiden yhdistämistä. Kun ne on yhdistetty, Rustilla on tarpeeksi tietoa salliakseen muistiturvalliset operaatiot ja kieltääkseen operaatiot, jotka luoisivat riippuvia osoittimia tai muuten rikkoisivat muistiturvallisuutta.

Struct-määrittelyissä

Tähän asti määrittelemämme structit säilyttävät kaikki omistettuja tyyppejä. Voimme määritellä structeja säilyttämään viittauksia, mutta siinä tapauksessa meidän täytyy lisätä elinikämerkintä jokaiseen viittaukseen structin määrittelyssä. Listauksessa 10-24 on struct nimeltä ImportantExcerpt, joka säilyttää merkkijonoviipaleen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-24/src/main.rs}}
}
Listing 10-24: Struct, joka säilyttää viittauksen ja vaatii elinikämerkinnän

Tällä structilla on yksi kenttä part, joka säilyttää merkkijonoviipaleen, joka on viittaus. Kuten geneeristen tietotyyppien kanssa, ilmoitamme geneerisen elinikäparametrin nimen kulmasulkeissa structin nimen jälkeen, jotta voimme käyttää elinikäparametria struct-määrittelyn rungossa. Tämä merkintä tarkoittaa, että ImportantExcerpt-instanssi ei voi elää pidempään kuin viittaus, jonka se säilyttää part-kentässään.

Tässä main-funktio luo ImportantExcerpt-structin instanssin, joka säilyttää viittauksen muuttujan novel omistaman String-arvon ensimmäiseen lauseeseen. novel-datan on olemassa ennen ImportantExcerpt-instanssin luomista. Lisäksi novel ei poistu näkyvyysalueelta ennen kuin ImportantExcerpt poistuu näkyvyysalueelta, joten ImportantExcerpt-instanssin viittaus on kelvollinen.

Elinikien poisjättäminen

Olet oppinut, että jokaisella viittauksella on elinikä ja että sinun täytyy määrittää elinikäparametrit funktioille tai structeille, jotka käyttävät viittauksia. Meillä oli kuitenkin funktio listauksessa 4-9, joka näytetään uudelleen listauksessa 10-25, ja se kääntyi ilman elinikämerkintöjä.

Filename: src/lib.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-25/src/main.rs:here}}
}
Listing 10-25: Funktio, jonka määrittelimme listauksessa 4-9 ja joka kääntyi ilman elinikämerkintöjä, vaikka parametri ja palautustyyppi ovat viittauksia

Syy, miksi tämä funktio kääntyy ilman elinikämerkintöjä, on historiallinen: Rustin varhaisissa versioissa (ennen 1.0) tämä koodi ei olisi kääntynyt, koska jokainen viittaus tarvitsi eksplisiittisen eliniän. Silloin funktion allekirjoitus olisi kirjoitettu näin:

fn first_word<'a>(s: &'a str) -> &'a str {

Kun Rust-tiimi oli kirjoittanut paljon Rust-koodia, he huomasivat, että Rust- ohjelmoijat kirjoittivat samoja elinikämerkintöjä yhä uudelleen tietyissä tilanteissa. Nämä tilanteet olivat ennustettavia ja noudattivat muutamia deterministisiä kuvioita. Kehittäjät ohjelmoivat nämä kuviot kääntäjän koodiin, jotta lainauskontrolleri voisi päätellä eliniät näissä tilanteissa eikä eksplisiittisiä merkintöjä tarvittaisi.

Tämä Rustin historia on merkityksellinen, koska on mahdollista, että lisää deterministisiä kuvioita ilmestyy ja lisätään kääntäjään. Tulevaisuudessa vähemmän elinikämerkintöjä saattaa olla tarpeen.

Kuviot, jotka on ohjelmoitu Rustin viittausten analyysiin, kutsutaan elinikien poisjättämissäännöiksi (lifetime elision rules). Nämä eivät ole sääntöjä ohjelmoijille noudatettavaksi; ne ovat joukko erityisiä tapauksia, joita kääntäjä harkitsee, ja jos koodisi sopii näihin tapauksiin, sinun ei tarvitse kirjoittaa eliniöitä eksplisiittisesti.

Poisjättämissäännöt eivät tarjoa täyttä päätelmää. Jos viittausten eliniöistä on vielä epäselvyyttä Rustin sovellettua sääntöjä, kääntäjä ei arvaa, mikä jäljellä olevien viittausten elinikä pitäisi olla. Arvaamisen sijaan kääntäjä antaa virheen, jonka voit ratkaista lisäämällä elinikämerkinnät.

Funktio- tai metodiparametrien eliniöitä kutsutaan syöttöeliniöiksi (input lifetimes), ja palautusarvojen eliniöitä tuottoeliniöiksi (output lifetimes).

Kääntäjä käyttää kolmea sääntöä selvittääkseen viittausten eliniöitä, kun eksplisiittisiä merkintöjä ei ole. Ensimmäinen sääntö koskee syöttöeliniöitä, ja toinen ja kolmas sääntö koskevat tuottoeliniöitä. Jos kääntäjä pääsee kolmen säännön loppuun ja viittauksia on vielä, joiden eliniöitä se ei voi selvittää, kääntäjä pysähtyy virheeseen. Nämä säännöt koskevat fn-määrittelyjä sekä impl-lohkoja.

Ensimmäinen sääntö on, että kääntäjä määrittää elinikäparametrin jokaiselle parametrille, joka on viittaus. Toisin sanoen funktio, jolla on yksi parametri, saa yhden elinikäparametrin: fn foo<'a>(x: &'a i32); funktio, jolla on kaksi parametria, saa kaksi erillistä elinikäparametria: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); ja niin edelleen.

Toinen sääntö on, että jos on täsmälleen yksi syöttöelinikäparametri, se elinikä määritetään kaikille tuottoelinikäparametreille: fn foo<'a>(x: &'a i32) -> &'a i32.

Kolmas sääntö on, että jos on useita syöttöelinikäparametreja, mutta yksi niistä on &self tai &mut self, koska tämä on metodi, self:n elinikä määritetään kaikille tuottoelinikäparametreille. Tämä kolmas sääntö tekee metodeista paljon miellyttävämpiä lukea ja kirjoittaa, koska vähemmän symboleja on tarpeen.

Olkoon me olemme kääntäjä. Sovelletaan näitä sääntöjä selvittääksemme viittausten eliniöt first_word-funktion allekirjoituksessa listauksessa 10-25. Allekirjoitus alkaa ilman eliniöitä, jotka liittyvät viittauksiin:

fn first_word(s: &str) -> &str {

Sitten kääntäjä soveltaa ensimmäistä sääntöä, joka määrittää, että jokainen parametri saa oman eliniänsä. Kutsumme sitä 'a:ksi kuten tavallisesti, joten nyt allekirjoitus on tämä:

fn first_word<'a>(s: &'a str) -> &str {

Toinen sääntö pätee, koska on täsmälleen yksi syöttöelinikä. Toinen sääntö määrittää, että yhden syöttöparametrin elinikä määritetään tuottoelinikään, joten allekirjoitus on nyt tämä:

fn first_word<'a>(s: &'a str) -> &'a str {

Nyt kaikilla viittauksilla tässä funktioallekirjoituksessa on eliniät, ja kääntäjä voi jatkaa analyysiään ilman, että ohjelmoijan tarvitsee merkitä eliniöitä tässä funktioallekirjoituksessa.

Katsotaan toista esimerkkiä käyttäen longest-funktiota, jolla ei ollut elinikäparametreja, kun aloimme työskennellä sen kanssa listauksessa 10-20:

fn longest(x: &str, y: &str) -> &str {

Sovelletaan ensimmäistä sääntöä: Jokainen parametri saa oman eliniänsä. Tällä kertaa meillä on kaksi parametria yhden sijaan, joten meillä on kaksi elinikää:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Näet, että toinen sääntö ei päde, koska on useampi kuin yksi syöttöelinikä. Kolmas sääntö ei myöskään päde, koska longest on funktio eikä metodi, joten mikään parametreista ei ole self. Kun olemme käyneet läpi kaikki kolme sääntöä, emme ole vieläkään selvittäneet palautustyypin elinikää. Tämän vuoksi saimme virheen yrittäessämme kääntää listauksen 10-20 koodia: Kääntäjä kävi läpi elinikien poisjättämissäännöt, mutta ei silti voinut selvittää kaikkia viittausten eliniöitä allekirjoituksessa.

Koska kolmas sääntö pätee todella vain metodien allekirjoituksissa, katsomme eliniöitä tässä yhteydessä seuraavaksi nähdäksemme, miksi kolmas sääntö tarkoittaa, ettei meidän tarvitse merkitä eliniöitä metodien allekirjoituksissa usein.

Metodimäärittelyissä

Kun toteutamme metodeja structille, jolla on eliniöitä, käytämme samaa syntaksia kuin geneeristen tyyppiparametrien kanssa, kuten listauksessa 10-11. Missä ilmoitamme ja käytämme elinikäparametreja riippuu siitä, liittyvätkö ne structin kenttiin vai metodin parametreihin ja palautusarvoihin.

Structin kenttien elinikien nimet täytyy aina ilmoittaa impl-avainsanan jälkeen ja sitten käyttää structin nimen jälkeen, koska nämä eliniät ovat osa structin tyyppiä.

impl-lohkon sisällä olevissa metodien allekirjoituksissa viittaukset voivat liittyä structin kenttien viittausten elinikään tai olla riippumattomia. Lisäksi elinikien poisjättämissäännöt usein tekevät niin, ettei elinikämerkintöjä tarvita metodien allekirjoituksissa. Katsotaan joitakin esimerkkejä käyttäen structia nimeltä ImportantExcerpt, jonka määrittelimme listauksessa 10-24.

Ensin käytämme level-nimistä metodia, jonka ainoa parametri on viittaus self:ään ja jonka palautusarvo on i32, joka ei ole viittaus mihinkään:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:1st}}
}

Elinikäparametrin ilmoitus impl-avainsanan jälkeen ja sen käyttö tyypin nimen jälkeen ovat pakollisia, mutta ensimmäisen poisjättämissäännön ansiosta emme tarvitse merkitä viittauksen self:n elinikää.

Tässä on esimerkki, jossa kolmas elinikien poisjättämissääntö pätee:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:3rd}}
}

On kaksi syöttöelinikää, joten Rust soveltaa ensimmäistä elinikien poisjättämissääntöä ja antaa sekä &self:lle että announcement:ille omat eliniänsä. Sitten, koska yksi parametreista on &self, palautustyyppi saa &self:n eliniän, ja kaikki eliniät on huomioitu.

Staattinen elinikä

Yksi erityinen elinikä, josta meidän täytyy keskustella, on 'static, joka merkitsee, että kyseinen viittaus voi elää koko ohjelman keston. Kaikilla merkkijonoliteraaleilla on 'static-elinikä, jonka voimme merkitä näin:

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

Tämän merkkijonon teksti on tallennettu suoraan ohjelman binääritiedostoon, joka on aina käytettävissä. Siksi kaikkien merkkijonoliteraalien elinikä on 'static.

Saatat nähdä virheilmoituksissa ehdotuksia käyttää 'static-elinikää. Mutta ennen kuin määrittelet 'static-elinikää viittaukselle, mieti, elääkö viittauksesi todella koko ohjelmasi eliniän ja haluatko sen elävän. Useimmiten virheilmoitus, joka ehdottaa 'static-elinikää, johtuu yrityksestä luoda riippuva viittaus tai eliniöjen epäsuhtasta. Tällaisissa tapauksissa ratkaisu on korjata nämä ongelmat, ei määrittää 'static-elinikää.

Geneeriset tyyppiparametrit, trait-rajat ja eliniät yhdessä

Katsotaan lyhyesti syntaksia geneeristen tyyppiparametrien, trait-rajojen ja eliniöiden määrittelyyn yhdessä yhdessä funktiossa!

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-11-generics-traits-and-lifetimes/src/main.rs:here}}
}

Tämä on longest-funktio listauksesta 10-21, joka palauttaa pidemmän kahdesta merkkijonoviipaleesta. Mutta nyt siinä on ylimääräinen parametri nimeltä ann geneerisestä tyypistä T, joka voidaan täyttää millä tahansa tyypillä, joka toteuttaa Display-traitin where-lausekkeen määrittämällä tavalla. Tämä ylimääräinen parametri tulostetaan {}-merkinnällä, minkä vuoksi Display- trait-raja on tarpeen. Koska eliniät ovat eräänlaisia geneerisiä tyyppejä, elinikäparametrin 'a ja geneerisen tyyppiparametrin T ilmoitukset menevät samaan listaan kulmasulkeissa funktion nimen jälkeen.

Yhteenveto

Käsittelimme paljon tässä luvussa! Nyt kun tiedät geneerisistä tyyppiparametreista, traitteista ja trait-rajoista sekä geneerisistä elinikäparametreista, olet valmis kirjoittamaan koodia ilman toistoa, joka toimii monissa eri tilanteissa. Geneeriset tyyppiparametrit antavat sinun soveltaa koodia eri tyyppeihin. Traitit ja trait-rajat varmistavat, että vaikka tyypit ovat geneerisiä, niillä on koodin tarvitsema käyttäytyminen. Opit käyttämään elinikämerkintöjä varmistaaksesi, ettei tämä joustava koodi sisällä riippuvia viittauksia. Ja kaikki tämä analyysi tapahtuu käännösaikana, mikä ei vaikuta ajonaikaiseen suorituskykyyn!

Usko tai älä, näistä luvussa käsittelemistämme aiheista on vielä paljon opittavaa: Luku 18 käsittelee trait-olioita, jotka ovat toinen tapa käyttää traitteja. On myös monimutkaisempia skenaarioita, joissa tarvitset elinikämerkintöjä vain hyvin edistyneissä tilanteissa; niitä varten sinun kannattaa lukea Rust Reference. Mutta seuraavaksi opit kirjoittamaan testejä Rustissa, jotta voit varmistaa, että koodisi toimii odotetulla tavalla.

Automaattisten testien kirjoittaminen

Edsger W. Dijkstra kirjoitti vuonna 1972 esseessään The Humble Programmer:
“Ohjelmatestaus voi olla erittäin tehokas tapa osoittaa virheiden olemassaolo, mutta se on täysin riittämätön osoittamaan niiden puuttumista.”
Tämä ei kuitenkaan tarkoita, ettemme voisi yrittää testata niin paljon kuin mahdollista!

Ohjelmamme oikeellisuus tarkoittaa sitä, kuinka hyvin koodi toteuttaa haluamamme toiminnallisuuden. Rust on suunniteltu huomioimaan ohjelmien oikeellisuus tarkasti, mutta sen varmistaminen on monimutkaista eikä helppoa todistaa. Rustin tyyppijärjestelmä kantaa suuren osan tästä vastuusta, mutta sekään ei voi estää kaikkia virheitä. Siksi Rust sisältää kattavan tuen automaattisten testien kirjoittamiseen.

Oletetaan, että kirjoitamme funktion add_two, joka lisää annetulle luvulle kaksi. Funktion allekirjoitus määrittää, että se ottaa kokonaisluvun parametrina ja palauttaa kokonaisluvun tuloksena. Kun toteutamme ja käännämme tämän funktion, Rust suorittaa tyyppitarkistuksen ja lainantarkistuksen varmistaakseen, että emme esimerkiksi yritä antaa sille String-tyyppistä arvoa tai kelpaamatonta viittausta.

Mutta Rust ei voi tarkistaa, tekeekö funktio juuri sitä, mitä tarkoitimme – eli palauttaako se parametrin plus 2 eikä esimerkiksi plus 10 tai miinus 50! Tällaisissa tilanteissa testit ovat ratkaisevia.

Voimme kirjoittaa testejä varmistaaksemme, että kun annamme funktiolle add_two arvon 3, se palauttaa arvon 5. Voimme ajaa nämä testit aina, kun teemme muutoksia koodiin, varmistaaksemme, että aiemmin toiminut käyttäytyminen ei ole muuttunut.

Testaaminen on monimutkainen taito. Emme voi kattaa yhden luvun aikana kaikkea, mitä hyvien testien kirjoittamiseen liittyy, mutta tässä luvussa käsittelemme Rustin testausjärjestelmän mekaniikkaa. Käymme läpi annotaatiot ja makrot, joita voit käyttää testien kirjoittamiseen, testien ajamisen oletuskäyttäytymisen ja asetukset, sekä testien organisoinnin yksikkötesteihin ja integraatiotesteihin.

Miten testejä kirjoitetaan

Testien kirjoittaminen

Testit ovat Rust-funktioita, jotka varmistavat, että ei-testikoodi toimii odotetulla tavalla. Testifunktioiden rungot suorittavat tyypillisesti nämä kolme toimintoa:

  • Valmistele tarvittavat tiedot tai tila.
  • Suorita testattava koodi.
  • Varmista, että tulokset ovat odotetut.

Katsotaan Rustin tarjoamia ominaisuuksia erityisesti testien kirjoittamiseen, jotka sisältävät nämä toiminnot: test-attribuutti, muutamia makroja ja should_panic-attribuutin.

Testifunktioiden rakenne

Yksinkertaisimmillaan Rustin testi on funktio, joka on merkitty test- attribuutilla. Attribuutit ovat metatietoa Rust-koodin osista; yksi esimerkki on derive-attribuutti, jota käytimme structeissa luvussa 5. Muuttaaksesi funktion testifunktioksi, lisää #[test] riville ennen fn:ää. Kun ajat testisi cargo test -komennolla, Rust rakentaa testiajaminen binääritiedoston, joka ajaa merkityt funktiot ja raportoi, läpäiseekö kukin testifunktio vai epäonnistuuko se.

Aina kun luomme uuden kirjastoprojektin Cargolla, testimoduuli testifunktiolla luodaan automaattisesti meille. Tämä moduuli antaa mallin testien kirjoittamiseen, jotta sinun ei tarvitse etsiä tarkkaa rakennetta ja syntaksia joka kerta, kun aloitat uuden projektin. Voit lisätä niin monta lisätestifunktiota ja testimoduulia kuin haluat!

Tutkimme testien toiminnan eri puolia kokeilemalla mallitestiä ennen kuin testaamme varsinaista koodia. Sitten kirjoitamme joitakin tosielämän testejä, jotka kutsuvat kirjoittamaamme koodia ja varmistavat, että sen käyttäytyminen on oikea.

Luodaan uusi kirjastoprojekti nimeltä adder, joka laskee yhteen kaksi lukua:

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

adder-kirjastosi src/lib.rs-tiedoston sisällön pitäisi näyttää listaukselta 11-1.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/src/lib.rs}}
Listing 11-1: cargo new -komennon automaattisesti luoma koodi

Tiedosto alkaa esimerkki-add-funktiolla, jotta meillä on jotain testattavaa.

Toistaiseksi keskitytään pelkästään it_works-funktioon. Huomaa #[test]- merkintä: Tämä attribuutti ilmaisee, että tämä on testifunktio, joten testiajaminen tietää käsitellä tätä funktiota testinä. Meillä voi myös olla ei-testifunktioita tests-moduulissa yhteisten skenaarioiden valmisteluun tai yleisten operaatioiden suorittamiseen, joten meidän täytyy aina ilmaista, mitkä funktiot ovat testejä.

Esimerkkifunktion runko käyttää assert_eq!-makroa varmistaakseen, että result, joka sisältää add-funktion kutsun tuloksen arvoilla 2 ja 2, on yhtä suuri kuin 4. Tämä varmistus toimii esimerkkinä tyypillisestä testin muodosta. Ajetaan se nähdäksemme, että testi läpäisee.

cargo test -komento ajaa kaikki projektimme testit, kuten listauksessa 11-2.

{{#include ../listings/ch11-writing-automated-tests/listing-11-01/output.txt}}
Listing 11-2: Automaattisesti luodun testin ajamisen tuloste

Cargo käänsi ja ajoi testin. Näemme rivin running 1 test. Seuraava rivi näyttää luodun testifunktion nimen, nimeltä tests::it_works, ja että testin ajamisen tulos on ok. Yhteenveto test result: ok. tarkoittaa, että kaikki testit läpäisivät, ja osa 1 passed; 0 failed laskee läpäisseiden tai epäonnistuneiden testien määrän.

Testin voi merkitä ohitetuksi, jotta se ei aja tietyssä tilanteessa; käsittelemme tämän luvun myöhemmässä osiossa ”Testien ohittaminen, ellei niitä erikseen pyydetä”. Koska emme ole tehneet sitä tässä, yhteenveto näyttää 0 ignored. Voimme myös antaa cargo test -komennolle argumentin ajaa vain testit, joiden nimi vastaa merkkijonoa; tätä kutsutaan suodatukseksi (filtering), ja käsittelemme sen osiossa ”Testien osajoukon ajaminen nimen perusteella”. Tässä emme ole suodattaneet ajettavia testejä, joten yhteenvedon loppu näyttää 0 filtered out.

0 measured -tilasto koskee suorituskykytestejä (benchmark tests). Tällä hetkellä suorituskykytestit ovat saatavilla vain yönightly-Rustissa. Katso dokumentaatio suorituskykytesteistä saadaksesi lisätietoja.

Testitulosteen seuraava osa alkaen Doc-tests adder koskee dokumentaatiotestien tuloksia. Meillä ei ole vielä dokumentaatiotestejä, mutta Rust voi kääntää API-dokumentaatiossamme esiintyvät koodiesimerkit. Tämä ominaisuus auttaa pitämään dokumentaation ja koodin synkassa! Käsittelemme dokumentaatiotestien kirjoittamista luvun 14 osiossa ”Dokumentaatiokommentit testeinä”. Toistaiseksi ohitamme Doc-tests- tulosteen.

Aloitetaan testin mukauttaminen omiin tarpeisiimme. Ensin vaihdetaan it_works- funktion nimi eri nimeksi, kuten exploration, näin:

Filename: src/lib.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-01-changing-test-name/src/lib.rs}}

Sitten ajetaan cargo test uudelleen. Tuloste näyttää nyt exploration it_works- kohdan sijaan:

{{#include ../listings/ch11-writing-automated-tests/no-listing-01-changing-test-name/output.txt}}

Nyt lisätään toinen testi, mutta tällä kertaa tehdään testi, joka epäonnistuu! Testit epäonnistuvat, kun jokin testifunktiossa paniikkiutuu. Jokainen testi ajetaan uudessa säikeessä, ja kun pääsäie näkee testisäikeen kuolleen, testi merkitään epäonnistuneeksi. Luvussa 9 puhuimme, että yksinkertaisin tapa paniikkiutua on kutsua panic!-makroa. Syötä uusi testi funktiona nimeltä another, jolloin src/lib.rs-tiedostosi näyttää listaukselta 11-3.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-03/src/lib.rs}}
Listing 11-3: Toisen testin lisääminen, joka epäonnistuu, koska kutsumme panic!-makroa

Aja testit uudelleen cargo test -komennolla. Tulosteen pitäisi näyttää listaukselta 11-4, joka näyttää, että exploration-testimme läpäisi ja another epäonnistui.

{{#include ../listings/ch11-writing-automated-tests/listing-11-03/output.txt}}
Listing 11-4: Testitulokset, kun yksi testi läpäisee ja yksi epäonnistuu

ok-kohdan sijaan rivi test tests::another näyttää FAILED. Kaksi uutta osiota ilmestyy yksittäisten tulosten ja yhteenvedon väliin: Ensimmäinen näyttää yksityiskohtaisen syyn kunkin testin epäonnistumiselle. Tässä tapauksessa saamme tiedot, että tests::another epäonnistui, koska se paniikkiutui viestillä Make this test fail rivillä 17 src/lib.rs-tiedostossa. Seuraava osio listaa vain kaikkien epäonnistuneiden testien nimet, mikä on hyödyllistä, kun testejä ja paljon yksityiskohtaista epäonnistuneen testin tulostetta on paljon. Voimme käyttää epäonnistuneen testin nimeä ajamaan vain kyseisen testin debugataksemme sitä helpommin; puhumme lisää testien ajamisen tavoista osiossa ”Testien ajamisen hallinta”.

Yhteenvetorivi näkyy lopussa: Kaiken kaikkiaan testituloksemme on FAILED. Yksi testi läpäisi ja yksi epäonnistui.

Nyt kun olet nähnyt, miltä testitulokset näyttävät eri skenaarioissa, katsotaan joitakin muita makroja kuin panic!, jotka ovat hyödyllisiä testeissä.

Tulosten tarkistaminen assert!-makrolla

assert!-makro, jonka standardikirjasto tarjoaa, on hyödyllinen, kun haluat varmistaa, että jokin ehto testissä evaluoituu arvoksi true. Annetaan assert!-makrolle argumentti, joka evaluoituu totuusarvoksi. Jos arvo on true, mitään ei tapahdu ja testi läpäisee. Jos arvo on false, assert!- makro kutsuu panic!:a saadakseen testin epäonnistumaan. assert!-makron käyttö auttaa meitä tarkistamaan, että koodimme toimii tarkoittamallamme tavalla.

Luvussa 5, listauksessa 5-15, käytimme Rectangle-structia ja can_hold- metodia, jotka toistetaan tässä listauksessa 11-5. Laitetaan tämä koodi src/lib.rs-tiedostoon ja kirjoitetaan sille testejä assert!-makron avulla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-05/src/lib.rs}}
Listing 11-5: Rectangle-struct ja sen can_hold-metodi luvusta 5

can_hold-metodi palauttaa totuusarvon, mikä tekee siitä täydellisen käyttötapauksen assert!-makrolle. Listauksessa 11-6 kirjoitamme testin, joka testaa can_hold-metodia luomalla Rectangle-instanssin, jonka leveys on 8 ja korkeus 7, ja varmistamalla, että se voi sisältää toisen Rectangle-instanssin, jonka leveys on 5 ja korkeus 1.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-06/src/lib.rs:here}}
Listing 11-6: Testi can_hold-metodille, joka tarkistaa, voiko suurempi suorakulmio todella sisältää pienemmän

Huomaa use super::*; -rivi tests-moduulin sisällä. tests-moduuli on tavallinen moduuli, joka noudattaa tavallisia näkyvyyssääntöjä, joita käsittelimme luvussa 7 osiossa ”Polut moduulipuun kohteeseen viittaamiseen”. Koska tests-moduuli on sisämoduuli, meidän täytyy tuoda testattava koodi ulommassa moduulissa sisämoduulin näkyvyysalueelle. Käytämme tässä globia, joten kaikki ulommassa moduulissa määrittelemämme on käytettävissä tässä tests-moduulissa.

Olemme nimenneet testimme larger_can_hold_smaller, ja olemme luoneet kaksi tarvitsemaamme Rectangle-instanssia. Sitten kutsuimme assert!-makroa ja annoimme sille lausekkeen larger.can_hold(&smaller). Tämän lausekkeen pitäisi palauttaa true, joten testimme pitäisi läpäistä. Katsotaan!

{{#include ../listings/ch11-writing-automated-tests/listing-11-06/output.txt}}

Se läpäisee! Lisätään toinen testi, joka tällä kertaa varmistaa, ettei pienempi suorakulmio voi sisältää suurempaa suorakulmiota:

Filename: src/lib.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-02-adding-another-rectangle-test/src/lib.rs:here}}

Koska can_hold-funktion oikea tulos tässä tapauksessa on false, meidän täytyy kääntää tämä tulos ennen kuin annamme sen assert!-makrolle. Näin testimme läpäisee, jos can_hold palauttaa false:

{{#include ../listings/ch11-writing-automated-tests/no-listing-02-adding-another-rectangle-test/output.txt}}

Kaksi testiä läpäisee! Katsotaan nyt, mitä testituloksille tapahtuu, kun tuomme virheen koodiimme. Muutamme can_hold-metodin toteutusta korvaamalla suurempi-kuin-merkin (>) pienempi-kuin-merkillä (<) leveyksiä vertailtaessa:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-03-introducing-a-bug/src/lib.rs:here}}

Testien ajaminen nyt tuottaa seuraavan:

{{#include ../listings/ch11-writing-automated-tests/no-listing-03-introducing-a-bug/output.txt}}

Testimme löysivät virheen! Koska larger.width on 8 ja smaller.width on 5, leveyksien vertailu can_hold-metodissa palauttaa nyt false: 8 ei ole pienempi kuin 5.

Yhtäsuuruuden testaaminen assert_eq!- ja assert_ne!-makroilla

Yleinen tapa varmistaa toiminnallisuus on testata yhtäsuuruutta testattavan koodin tuloksen ja odottamasi arvon välillä. Voit tehdä tämän assert!-makrolla ja antamalla sille lausekkeen, joka käyttää ==-operaattoria. Tämä on kuitenkin niin yleinen testi, että standardikirjasto tarjoaa makroparin — assert_eq! ja assert_ne! — suorittaakseen tämän testin kätevämmin. Nämä makrot vertaavat kahta argumenttia yhtäsuuruuden tai erisuuruuden osalta. Ne myös tulostavat kaksi arvoa, jos varmistus epäonnistuu, mikä helpottaa miksi testi epäonnistui; päinvastoin assert!-makro ilmaisee vain, että se sai false-arvon ==- lausekkeelle tulostamatta arvoja, jotka johtivat false-arvoon.

Listauksessa 11-7 kirjoitamme funktion nimeltä add_two, joka lisää 2 parametriinsa, ja testaamme sitten tämän funktion assert_eq!-makrolla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-07/src/lib.rs}}
Listing 11-7: Funktion add_two testaaminen assert_eq!-makrolla

Tarkistetaan, että se läpäisee!

{{#include ../listings/ch11-writing-automated-tests/listing-11-07/output.txt}}

Luomme muuttujan nimeltä result, joka sisältää add_two(2)-kutsun tuloksen. Sitten annamme result- ja 4-arvot assert_eq!-makrolle. Tulostusrivi tälle testille on test tests::it_adds_two ... ok, ja ok-teksti ilmaisee, että testimme läpäisi!

Tuodaan virhe koodiimme nähdäksemme, miltä assert_eq! näyttää epäonnistuessaan. Muutetaan add_two-funktion toteutus lisäämään 3 2:n sijaan:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-04-bug-in-add-two/src/lib.rs:here}}

Aja testit uudelleen:

{{#include ../listings/ch11-writing-automated-tests/no-listing-04-bug-in-add-two/output.txt}}

Testimme löysi virheen! tests::it_adds_two -testi epäonnistui, ja viesti kertoo, että epäonnistunut varmistus oli left == right ja mitkä left- ja right-arvot ovat. Tämä viesti auttaa aloittamaan debuggauksen: left- argumentti, jossa meillä oli add_two(2)-kutsun tulos, oli 5, mutta right- argumentti oli 4. Voit kuvitella, että tästä olisi erityisen hyötyä, kun testejä on paljon.

Huomaa, että joissakin kielissä ja testikehyksissä yhtäsuuruusvarmistusten funktioiden parametreja kutsutaan expected- ja actual-arvoiksi, ja argumenttien järjestys on merkityksellinen. Rustissa niitä kutsutaan kuitenkin left- ja right-arvoiksi, eikä sillä ole väliä, missä järjestyksessä määrittelemme odottamamme arvon ja koodin tuottaman arvon. Voisimme kirjoittaa testin varmistuksen muodossa assert_eq!(4, result), mikä johtaisi samaan epäonnistumisviestiin, joka näyttää assertion `left == right` failed.

assert_ne!-makro läpäisee, jos kaksi antamaamme arvoa eivät ole yhtä suuret, ja epäonnistuu, jos ne ovat. Tämä makro on hyödyllisin tapauksissa, joissa emme ole varmoja, mikä arvo tulee olemaan, mutta tiedämme, mikä arvo ei todellakaan saa olla. Esimerkiksi jos testaamme funktiota, joka muuttaa syötteensä jollakin tavalla, mutta tapa, jolla syöte muuttuu, riippuu viikonpäivästä, jolloin ajamme testejämme, paras asia varmistaa saattaa olla, ettei funktion tuloste ole yhtä suuri kuin syöte.

Pinnan alla assert_eq!- ja assert_ne!-makrot käyttävät operaattoreita == ja !=. Kun varmistukset epäonnistuvat, nämä makrot tulostavat argumenttinsa debug-muotoilulla, mikä tarkoittaa, että verrattavien arvojen täytyy toteuttaa PartialEq- ja Debug-traitit. Kaikki primitiivityypit ja useimmat standardikirjaston tyypit toteuttavat nämä traitit. Itse määrittelemillesi structeille ja enum-arvoille sinun täytyy toteuttaa PartialEq varmistaaksesi näiden tyyppien yhtäsuuruuden. Sinun täytyy myös toteuttaa Debug tulostaaksesi arvot, kun varmistus epäonnistuu. Koska molemmat traitit ovat johdettavia traitteja, kuten mainittiin listauksessa 5-12 luvussa 5, tämä on yleensä yhtä suoraviivaista kuin #[derive(PartialEq, Debug)]-merkinnän lisääminen struct- tai enum-määrittelyysi. Katso liite C, ”Johdettavat traitit” näistä ja muista johdettavista traiteista.

Mukautettujen virheviestien lisääminen

Voit myös lisätä mukautetun viestin, joka tulostetaan epäonnistumisviestin kanssa, valinnaisina argumentteina assert!-, assert_eq!- ja assert_ne!- makroille. Pakollisten argumenttien jälkeen määritellyt argumentit välitetään format!-makrolle (käsiteltiin luvussa 8 osiossa ”Yhdistäminen +- tai format!-operaattorilla”), joten voit antaa muotoilumerkkijonon, joka sisältää {}-paikkamerkit ja arvot näihin paikkamerkkeihin. Mukautetut viestit ovat hyödyllisiä dokumentoimaan, mitä varmistus tarkoittaa; kun testi epäonnistuu, sinulla on parempi käsitys siitä, mikä koodissa on vikana verrattuna siihen, mitä odotimme tapahtuvan.

Esimerkiksi sanotaan, että meillä on funktio, joka tervehtii ihmisiä nimellä, ja haluamme testata, että funktiolle antamamme nimi näkyy tulosteessa:

Filename: src/lib.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-05-greeter/src/lib.rs}}

Tämän ohjelman vaatimuksista ei ole vielä sovittu, ja olemme melko varmoja, että tervehdyksen alussa oleva Hello-teksti muuttuu. Päätimme, ettei meidän haluta päivittää testiä, kun vaatimukset muuttuvat, joten tarkistamme tarkan yhtäsuuruuden greeting-funktion palauttamaan arvoon sen sijaan, että varmistaisimme vain, että tuloste sisältää syöteparametrin tekstin.

Tuodaan nyt virhe tähän koodiin muuttamalla greeting jättämään name pois nähdäksemme, miltä oletusarvoinen testin epäonnistuminen näyttää:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/src/lib.rs:here}}

Tämän testin ajaminen tuottaa seuraavan:

{{#include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/output.txt}}

Tämä tulos ilmaisee vain, että varmistus epäonnistui ja millä rivillä varmistus on. Hyödyllisempi epäonnistumisviesti tulostaisi greeting-funktion arvon. Lisätään mukautettu epäonnistumisviesti, joka koostuu muotoilumerkkijonosta paikkamerkillä, joka täytetään todellisella arvolla, jonka saimme greeting- funktiosta:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-07-custom-failure-message/src/lib.rs:here}}

Nyt kun ajamme testin, saamme informatiivisemman virheviestin:

{{#include ../listings/ch11-writing-automated-tests/no-listing-07-custom-failure-message/output.txt}}

Näemme testitulosteessa todellisen arvon, jonka saimme, mikä auttaisi debuggaamaan, mitä tapahtui sen sijaan, mitä odotimme tapahtuvan.

Paniikin tarkistaminen should_panic-attribuutilla

Palautusarvojen tarkistamisen lisäksi on tärkeää tarkistaa, että koodimme käsittelee virhetilanteet odottamallamme tavalla. Esimerkiksi harkitse Guess-tyyppiä, jonka loimme luvussa 9, listauksessa 9-13. Muu koodi, joka käyttää Guess-tyyppiä, luottaa takuuseen, että Guess-instanssit sisältävät vain arvoja välillä 1–100. Voimme kirjoittaa testin, joka varmistaa, että Guess-instanssin luominen arvolla tämän alueen ulkopuolelta paniikkiutuu.

Teemme tämän lisäämällä should_panic-attribuutin testifunktiollemme. Testi läpäisee, jos funktion sisällä oleva koodi paniikkiutuu; testi epäonnistuu, jos funktion sisällä oleva koodi ei paniikkiudu.

Listaus 11-8 näyttää testin, joka tarkistaa, että Guess::new-funktion virhetilanteet tapahtuvat, kun odotamme niitä.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-08/src/lib.rs}}
Listing 11-8: Testaus, että ehto aiheuttaa panic!-kutsun

Sijoitamme #[should_panic]-attribuutin #[test]-attribuutin jälkeen ja ennen testifunktiota, johon se kohdistuu. Katsotaan tulos, kun tämä testi läpäisee:

{{#include ../listings/ch11-writing-automated-tests/listing-11-08/output.txt}}

Näyttää hyvältä! Tuodaan nyt virhe koodiimme poistamalla ehto, jonka perusteella new-funktio paniikkiutuu, jos arvo on suurempi kuin 100:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-08-guess-with-bug/src/lib.rs:here}}

Kun ajamme listauksen 11-8 testin, se epäonnistuu:

{{#include ../listings/ch11-writing-automated-tests/no-listing-08-guess-with-bug/output.txt}}

Emme saa tässä tapauksessa kovin hyödyllistä viestiä, mutta kun katsomme testifunktiota, näemme, että se on merkitty #[should_panic]-attribuutilla. Saamamme epäonnistuminen tarkoittaa, että testifunktion koodi ei aiheuttanut paniikkia.

should_panic-attribuuttia käyttävät testit voivat olla epätarkkoja. should_panic-testi läpäisisi, vaikka testi paniikkiutuisi eri syystä kuin odotimme. Tehdäksemme should_panic-testeistä tarkempia voimme lisätä valinnaisen expected-parametrin should_panic-attribuuttiin. Testiajaminen varmistaa, että epäonnistumisviesti sisältää annetun tekstin. Esimerkiksi harkitse muokattua Guess-koodia listauksessa 11-9, jossa new-funktio paniikkiutuu eri viesteillä riippuen siitä, onko arvo liian pieni vai liian suuri.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-09/src/lib.rs:here}}
Listing 11-9: Testaus panic!-kutsulle, jonka paniikkiviesti sisältää määritellyn osamerkkijonon

Tämä testi läpäisee, koska arvo, jonka laitamme should_panic-attribuutin expected-parametriin, on osamerkkijono viestistä, jolla Guess::new-funktio paniikkiutuu. Olisimme voineet määrittää koko odottamamme paniikkiviestin, joka tässä tapauksessa olisi Guess value must be less than or equal to 100, got 200. Mitä määrität riippuu siitä, kuinka uniikki tai dynaaminen paniikkiviesti on ja kuinka tarkka haluat testisi olevan. Tässä tapauksessa paniikkiviestin osamerkkijono riittää varmistamaan, että testifunktion koodi suorittaa else if value > 100 -tapauksen.

Nähdäksemme, mitä tapahtuu, kun should_panic-testi expected-viestillä epäonnistuu, tuodaan taas virhe koodiimme vaihtamalla if value < 1- ja else if value > 100 -lohkojen rungot:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-09-guess-with-panic-msg-bug/src/lib.rs:here}}

Tällä kertaa kun ajamme should_panic-testin, se epäonnistuu:

{{#include ../listings/ch11-writing-automated-tests/no-listing-09-guess-with-panic-msg-bug/output.txt}}

Epäonnistumisviesti ilmaisee, että tämä testi todellakin paniikkiutui kuten odotimme, mutta paniikkiviesti ei sisältänyt odotettua merkkijonoa less than or equal to 100. Paniikkiviesti, jonka saimme tässä tapauksessa, oli Guess value must be greater than or equal to 1, got 200. Nyt voimme alkaa selvittää, missä virheemme on!

Result<T, E>-tyypin käyttö testeissä

Kaikki testimme tähän asti paniikkiutuvat epäonnistuessaan. Voimme myös kirjoittaa testejä, jotka käyttävät Result<T, E>-tyyppiä! Tässä on testi listauksesta 11-1, kirjoitettu uudelleen käyttämään Result<T, E>-tyyppiä ja palauttamaan Err paniikkiutumisen sijaan:

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-10-result-in-tests/src/lib.rs:here}}

it_works-funktiolla on nyt palautustyyppi Result<(), String>. Funktion rungossa kutsumme assert_eq!-makron sijaan Ok(()), kun testi läpäisee, ja Err-arvoa, jonka sisällä on String, kun testi epäonnistuu.

Testien kirjoittaminen niin, että ne palauttavat Result<T, E>-tyypin, mahdollistaa kysymysmerkkioperaattorin käytön testien rungossa, mikä voi olla kätevä tapa kirjoittaa testejä, joiden pitäisi epäonnistua, jos mikä tahansa operaatio niiden sisällä palauttaa Err-variantin.

Et voi käyttää #[should_panic]-merkintää testeissä, jotka käyttävät Result<T, E>-tyyppiä. Varmistaaksesi, että operaatio palauttaa Err-variantin, älä käytä kysymysmerkkioperaattoria Result<T, E>-arvolla. Sen sijaan käytä assert!(value.is_err()).

Nyt kun tiedät useita tapoja kirjoittaa testejä, katsotaan, mitä tapahtuu, kun ajamme testejämme, ja tutkitaan eri vaihtoehtoja, joita voimme käyttää cargo test -komennolla.

Testien suorituksen hallinta

Testien ajamisen hallinta

Aivan kuten cargo run kääntää koodisi ja ajaa syntyneen binääritiedoston, cargo test kääntää koodisi testitilassa ja ajaa syntyneen testibinääritiedoston. cargo test -komennon tuottaman binääritiedoston oletuskäyttäytyminen on ajaa kaikki testit rinnakkain ja kaapata testien aikana syntynyt tuloste, estäen tulosteen näyttämisen ja helpottaen testituloksiin liittyvän tulosteen lukemista. Voit kuitenkin määrittää komentorivivalintoja muuttaaksesi tätä oletuskäyttäytymistä.

Jotkut komentorivivalinnat menevät cargo test -komennolle ja jotkut syntyneelle testibinääritiedostolle. Erottaaksesi nämä kaksi argumenttityyppiä, listaa cargo test -komennolle menevät argumentit, sitten erotin -- ja sitten testibinääritiedostolle menevät argumentit. cargo test --help näyttää valinnat, joita voit käyttää cargo test -komennolla, ja cargo test -- --help näyttää valinnat, joita voit käyttää erottimen jälkeen. Nämä valinnat on myös dokumentoitu The rustc Book -kirjan osiossa ”Tests”.

Testien ajaminen rinnakkain tai peräkkäin

Kun ajat useita testejä, ne ajetaan oletusarvoisesti rinnakkain säikeitä käyttäen, mikä tarkoittaa, että ne valmistuvat nopeammin ja saat palautetta aikaisemmin. Koska testit ajetaan samaan aikaan, sinun täytyy varmistaa, etteivät testisi riipu toisistaan tai jaetusta tilasta, mukaan lukien jaettu ympäristö, kuten nykyinen työhakemisto tai ympäristömuuttujat.

Esimerkiksi sanotaan, että jokainen testisi ajaa koodia, joka luo levylle tiedoston nimeltä test-output.txt ja kirjoittaa siihen dataa. Sitten jokainen testi lukee tiedoston datan ja varmistaa, että tiedosto sisältää tietyn arvon, joka on eri jokaisessa testissä. Koska testit ajetaan samaan aikaan, yksi testi saattaa ylikirjoittaa tiedoston aikana, jolloin toinen testi kirjoittaa ja lukee tiedostoa. Toinen testi epäonnistuu silloin, ei siksi että koodi olisi virheellinen, vaan siksi että testit häiritsivät toisiaan ajettaessa rinnakkain. Yksi ratkaisu on varmistaa, että jokainen testi kirjoittaa eri tiedostoon; toinen ratkaisu on ajaa testit yksi kerrallaan.

Jos et halua ajaa testejä rinnakkain tai haluat tarkemman hallinnan käytettävien säikeiden määrästä, voit lähettää --test-threads-lipun ja haluamasi säikeiden määrän testibinääritiedostolle. Katso seuraava esimerkki:

$ cargo test -- --test-threads=1

Asetamme testisäikeiden määräksi 1, kertoen ohjelmalle, ettei se käytä rinnakkaisuutta. Testien ajaminen yhdellä säikeellä kestää kauemmin kuin rinnakkain, mutta testit eivät häiritse toisiaan, jos ne jakavat tilaa.

Funktioiden tulosteen näyttäminen

Oletusarvoisesti, jos testi läpäisee, Rustin testikirjasto kaappaa kaiken vakiotulosteeseen tulostetun. Esimerkiksi jos kutsumme println!:a testissä ja testi läpäisee, emme näe println!-tulostetta terminaalissa; näemme vain rivin, joka ilmaisee testin läpäisseen. Jos testi epäonnistuu, näemme kaiken vakiotulosteeseen tulostetun epäonnistumisviestin mukana.

Esimerkkinä listauksessa 11-10 on hassu funktio, joka tulostaa parametriarvonsa ja palauttaa 10, sekä testi, joka läpäisee, ja testi, joka epäonnistuu.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-10/src/lib.rs}}
Listing 11-10: Testit funktiolle, joka kutsuu println!-makroa

Kun ajamme nämä testit cargo test -komennolla, näemme seuraavan tulosteen:

{{#include ../listings/ch11-writing-automated-tests/listing-11-10/output.txt}}

Huomaa, ettei tässä tulosteessa missään näy I got the value 4, joka tulostetaan, kun läpäisevä testi ajetaan. Tuo tuloste on kaapattu. Epäonnistuneen testin tuloste I got the value 8 näkyy testiyhteenvedon osiossa, joka näyttää myös testin epäonnistumisen syyn.

Jos haluamme nähdä läpäisevien testien tulostetut arvot, voimme kertoa Rustille näyttämään myös onnistuneiden testien tulosteen --show-output-lipulla:

$ cargo test -- --show-output

Kun ajamme listauksen 11-10 testit uudelleen --show-output-lipulla, näemme seuraavan tulosteen:

{{#include ../listings/ch11-writing-automated-tests/output-only-01-show-output/output.txt}}

Testien osajoukon ajaminen nimen perusteella

Koko testisarjan ajaminen voi joskus kestää kauan. Jos työskentelet tietyn alueen koodin parissa, saatat haluta ajaa vain kyseiseen koodiin liittyvät testit. Voit valita, mitkä testit ajetaan, antamalla cargo test -komennolle ajettavien testien nimen tai nimet argumenttina.

Demonstroidaksemme, miten testien osajoukko ajetaan, luomme ensin kolme testiä add_two-funktiollemme, kuten listauksessa 11-11, ja valitsemme, mitkä niistä ajetaan.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-11/src/lib.rs}}
Listing 11-11: Kolme testiä kolmella eri nimellä

Jos ajamme testit antamatta argumentteja, kuten näimme aiemmin, kaikki testit ajetaan rinnakkain:

{{#include ../listings/ch11-writing-automated-tests/listing-11-11/output.txt}}

Yksittäisten testien ajaminen

Voimme antaa cargo test -komennolle minkä tahansa testifunktion nimen ajaa vain kyseisen testin:

{{#include ../listings/ch11-writing-automated-tests/output-only-02-single-test/output.txt}}

Vain one_hundred-niminen testi ajettiin; kaksi muuta testiä ei vastannut tätä nimeä. Testituloste kertoo, että meillä oli enemmän testejä, joita ei ajettu, näyttämällä lopussa 2 filtered out.

Emme voi määrittää useita testinimiä tällä tavalla; vain ensimmäistä cargo test -komennolle annettua arvoa käytetään. Mutta on tapa ajaa useita testejä.

Suodatus useiden testien ajamiseen

Voimme määrittää osan testinimestä, ja jokainen testi, jonka nimi vastaa tätä arvoa, ajetaan. Esimerkiksi koska kahden testimme nimi sisältää add, voimme ajaa nämä kaksi ajamalla cargo test add:

{{#include ../listings/ch11-writing-automated-tests/output-only-03-multiple-tests/output.txt}}

Tämä komento ajoi kaikki testit, joiden nimessä on add, ja suodatti pois one_hundred-nimisen testin. Huomaa myös, että moduuli, jossa testi esiintyy, tulee osaksi testin nimeä, joten voimme ajaa kaikki moduulin testit suodattamalla moduulin nimen perusteella.

Testien ohittaminen, ellei niitä erikseen pyydetä

Joskus muutama tietty testi voi olla hyvin aikaa vievä suorittaa, joten saatat haluta jättää ne pois useimmista cargo test -ajoista. Sen sijaan, että listaisit argumentteina kaikki testit, jotka haluat ajaa, voit merkitä aikaa vievät testit ignore-attribuutilla poissulkemista varten, kuten tässä:

Filename: src/lib.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-11-ignore-a-test/src/lib.rs:here}}

#[test]-merkinnän jälkeen lisäämme #[ignore]-rivin testiin, jonka haluamme jättää pois. Nyt kun ajamme testejämme, it_works ajetaan, mutta expensive_test ei:

{{#include ../listings/ch11-writing-automated-tests/no-listing-11-ignore-a-test/output.txt}}

expensive_test-funktio on listattu ignored-tilassa. Jos haluamme ajaa vain ohitetut testit, voimme käyttää cargo test -- --ignored:

{{#include ../listings/ch11-writing-automated-tests/output-only-04-running-ignored/output.txt}}

Hallitsemalla, mitkä testit ajetaan, voit varmistaa, että cargo test -tulokset palautuvat nopeasti. Kun olet siinä vaiheessa, että on järkevää tarkistaa ignored-testien tulokset ja sinulla on aikaa odottaa tuloksia, voit ajaa cargo test -- --ignored sen sijaan. Jos haluat ajaa kaikki testit riippumatta siitä, ovatko ne ohitettuja vai eivät, voit ajaa cargo test -- --include-ignored.

Testien organisointi

Testien organisointi

Kuten luvun alussa mainittiin, testaus on monimutkainen ala, ja eri ihmiset käyttävät eri terminologiaa ja organisointia. Rust-yhteisö ajattelee testejä kahden pääkategorian kautta: yksikkötestit ja integraatiotestit. Yksikkötestit ovat pieniä ja keskittyneempiä, testaten yhtä moduulia erillään muusta koodista kerrallaan, ja ne voivat testata yksityisiä rajapintoja. Integraatiotestit ovat täysin kirjastosi ulkopuolella ja käyttävät koodiasi samalla tavalla kuin mikä tahansa muu ulkoinen koodi, käyttäen vain julkista rajapintaa ja mahdollisesti testaten useita moduuleja per testi.

Molempien testityyppien kirjoittaminen on tärkeää varmistaaksesi, että kirjastosi osat tekevät odottamasi asiat erikseen ja yhdessä.

Yksikkötestit

Yksikkötestien tarkoitus on testata jokainen koodin yksikkö erillään muusta koodista nopeasti paikantaakseen, missä koodi toimii ja missä ei odotetulla tavalla. Laitat yksikkötestit src-hakemistoon kuhunkin tiedostoon sen koodin kanssa, jota ne testaavat. Käytäntö on luoda tests-niminen moduuli kuhunkin tiedostoon testifunktioiden säilyttämiseksi ja merkitä moduuli cfg(test)- attribuutilla.

tests-moduuli ja #[cfg(test)]

#[cfg(test)]-merkintä tests-moduulissa kertoo Rustille kääntää ja ajaa testikoodin vain, kun ajat cargo test, ei kun ajat cargo build. Tämä säästää käännösaikaa, kun haluat vain rakentaa kirjaston, ja säästää tilaa syntyneessä käännettyssä artefaktissa, koska testejä ei sisällytetä. Näet, että koska integraatiotestit menevät eri hakemistoon, niillä ei tarvitse olla #[cfg(test)]- merkintää. Koska yksikkötestit menevät samoihin tiedostoihin koodin kanssa, käytät #[cfg(test)]:ää määrittääksesi, ettei niitä sisällytetä käännettyyn tulokseen.

Muista, että kun loimme uuden adder-projektin tämän luvun ensimmäisessä osiossa, Cargo loi meille tämän koodin:

Filename: src/lib.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/src/lib.rs}}

Automaattisesti luodussa tests-moduulissa cfg-attribuutti tarkoittaa konfiguraatiota (configuration) ja kertoo Rustille, että seuraava kohde tulisi sisällyttää vain tietyn konfiguraatiovaihtoehdon ollessa voimassa. Tässä tapauksessa konfiguraatiovaihtoehto on test, jonka Rust tarjoaa testien kääntämiseen ja ajamiseen. Käyttämällä cfg-attribuuttia Cargo kääntää testikoodimme vain, jos aktiivisesti ajamme testit cargo test -komennolla. Tämä sisältää kaikki apufunktiot, jotka voivat olla tässä moduulissa, sekä #[test]-merkityt funktiot.

Yksityisten funktioiden testaaminen

Testausyhteisössä on keskustelua siitä, pitäisikö yksityisiä funktioita testata suoraan, ja muut kielet tekevät yksityisten funktioiden testaamisesta vaikeaa tai mahdotonta. Riippumatta siitä, mihin testausideologiaan sitoudut, Rustin näkyvyyssäännöt sallivat yksityisten funktioiden testaamisen. Harkitse listauksen 11-12 koodia yksityisellä internal_adder-funktiolla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-12/src/lib.rs}}
Listing 11-12: Yksityisen funktion testaaminen

Huomaa, että internal_adder-funktiota ei ole merkitty pub-määritteellä. Testit ovat vain Rust-koodia, ja tests-moduuli on vain toinen moduuli. Kuten käsiteltiin osiossa ”Polut moduulipuun kohteeseen viittaamiseen”, lapsimoduulien kohteet voivat käyttää esi-isämoduuliensa kohteita. Tässä testissä tuomme kaikki tests-moduulin vanhemman moduulin kohteet näkyviin use super::* -lauseella, ja sitten testi voi kutsua internal_adder-funktiota. Jos et mielestäsi yksityisiä funktioita pitäisi testata, Rustissa ei ole mitään, mikä pakottaisi sinua tekemään niin.

Integraatiotestit

Rustissa integraatiotestit ovat täysin kirjastosi ulkopuolella. Ne käyttävät kirjastoasi samalla tavalla kuin mikä tahansa muu koodi, mikä tarkoittaa, että ne voivat kutsua vain kirjastosi julkisen API:n funktioita. Niiden tarkoitus on testata, toimivatko kirjastosi monet osat yhdessä oikein. Koodin yksiköt, jotka toimivat oikein yksinään, voivat aiheuttaa ongelmia integroituna, joten integroidun koodin testikattavuus on myös tärkeää. Luodaksesi integraatiotestejä tarvitset ensin tests-hakemiston.

tests-hakemisto

Luomme tests-hakemiston projektihakemiston ylätasolle, src-hakemiston viereen. Cargo tietää etsiä integraatiotestitiedostoja tästä hakemistosta. Voimme sitten tehdä niin monta testitiedostoa kuin haluamme, ja Cargo kääntää jokaisen tiedoston erillisenä cratena.

Luodaan integraatiotesti. Kun listauksen 11-12 koodi on vielä src/lib.rs- tiedostossa, tee tests-hakemisto ja luo uusi tiedosto nimeltä tests/integration_test.rs. Hakemistorakenteesi pitäisi näyttää tältä:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

Syötä listauksen 11-13 koodi tests/integration_test.rs-tiedostoon.

Filename: tests/integration_test.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-13/tests/integration_test.rs}}
Listing 11-13: Integraatiotesti adder-craten funktiolle

Jokainen tiedosto tests-hakemistossa on erillinen crate, joten meidän täytyy tuoda kirjastomme kunkin testicraten näkyvyysalueelle. Siksi lisäämme use adder::add_two; koodin alkuun, jota emme tarvinne yksikkötesteissä.

Emme tarvitse merkitä mitään koodia tests/integration_test.rs-tiedostossa #[cfg(test)]-attribuutilla. Cargo käsittelee tests-hakemistoa erityisesti ja kääntää tämän hakemiston tiedostot vain, kun ajamme cargo test. Aja cargo test nyt:

{{#include ../listings/ch11-writing-automated-tests/listing-11-13/output.txt}}

Tulosteen kolme osiota sisältävät yksikkötestit, integraatiotestin ja dokumentaatiotestit. Huomaa, että jos mikä tahansa testi osiossa epäonnistuu, seuraavia osioita ei ajeta. Esimerkiksi jos yksikkötesti epäonnistuu, integraatio- ja dokumentaatiotesteistä ei ole tulostetta, koska nämä testit ajetaan vain, jos kaikki yksikkötestit läpäisevät.

Yksikkötestien ensimmäinen osio on sama kuin olemme nähneet: yksi rivi kullekin yksikkötestille (yksi nimeltä internal, jonka lisäsimme listauksessa 11-12) ja sitten yhteenvetorivi yksikkötesteille.

Integraatiotestien osio alkaa rivillä Running tests/integration_test.rs. Seuraavaksi on rivi kullekin integraatiotestin testifunktiolle ja yhteenvetorivi integraatiotestin tuloksille juuri ennen kuin Doc-tests adder- osio alkaa.

Jokaisella integraatiotestitiedostolla on oma osionsa, joten jos lisäämme lisää tiedostoja tests-hakemistoon, tulee lisää integraatiotestien osioita.

Voimme silti ajaa tietyn integraatiotestifunktion määrittämällä testifunktion nimen argumentiksi cargo test -komennolle. Ajaaaksesi kaikki testit tietyssä integraatiotestitiedostossa, käytä cargo test -komennon --test-argumenttia ja tiedoston nimeä:

{{#include ../listings/ch11-writing-automated-tests/output-only-05-single-integration/output.txt}}

Tämä komento ajaa vain tests/integration_test.rs-tiedoston testit.

Alimodulit integraatiotesteissä

Kun lisäät integraatiotestejä, saatat haluta tehdä lisää tiedostoja tests- hakemistoon järjestääksesi niitä; esimerkiksi voit ryhmitellä testifunktiot sen toiminnallisuuden mukaan, jota ne testaavat. Kuten aiemmin mainittiin, jokainen tiedosto tests-hakemistossa käännetään omana erillisenä cratenaan, mikä on hyödyllistä erillisten näkyvyysalueiden luomiseen, jotka jäljittelevät läheisemmin sitä, miten loppukäyttäjät käyttävät crateasi. Tämä tarkoittaa kuitenkin, että tests-hakemiston tiedostot eivät jaa samaa käyttäytymistä kuin src-hakemiston tiedostot, kuten opit luvussa 7 koodin jakamisesta moduuleihin ja tiedostoihin.

tests-hakemiston tiedostojen erilainen käyttäytyminen näkyy selvimmin, kun sinulla on joukko apufunktioita useissa integraatiotestitiedostoissa, ja yrität seurata luvun 7 osion ”Moduulien erottaminen eri tiedostoihin” vaiheita erottaaksesi ne yhteiseen moduuliin. Esimerkiksi jos luomme tests/common.rs-tiedoston ja sijoitamme siihen setup-nimisen funktion, voimme lisätä setup-funktioon koodia, jota haluamme kutsua useista testifunktioista useissa testitiedostoissa:

Filename: tests/common.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-12-shared-test-code-problem/tests/common.rs}}

Kun ajamme testit uudelleen, näemme uuden osion testitulosteessa common.rs- tiedostolle, vaikka tässä tiedostossa ei ole testifunktioita eikä me kutsunut setup-funktiota mistään:

{{#include ../listings/ch11-writing-automated-tests/no-listing-12-shared-test-code-problem/output.txt}}

common-näkyminen testituloksissa running 0 tests -tulosteella ei ole sitä, mitä halusimme. Halusimme vain jakaa koodia muiden integraatiotestitiedostojen kanssa. Välttääksemme common-näkymisen testitulosteessa, sen sijaan että loisimme tests/common.rs-tiedoston, luomme tests/common/mod.rs-tiedoston. Projektihakemisto näyttää nyt tältä:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

Tämä on vanhempi nimeämiskäytäntö, jonka Rust myös ymmärtää ja josta mainittiin luvun 7 osiossa ”Vaihtoehtoiset tiedostopolut”. Tämän niminen tiedosto kertoo Rustille, ettei se käsittele common-moduulia integraatiotestitiedostona. Kun siirrämme setup-funktion koodin tests/common/mod.rs-tiedostoon ja poistamme tests/common.rs-tiedoston, testitulosteen osio ei enää näy. tests-hakemiston alihakemistojen tiedostoja ei käännetä erillisinä crateina eikä niillä ole osioita testitulosteessa.

Kun olemme luoneet tests/common/mod.rs-tiedoston, voimme käyttää sitä miltä tahansa integraatiotestitiedostolta moduulina. Tässä on esimerkki setup-funktion kutsumisesta it_adds_two-testistä tests/integration_test.rs- tiedostossa:

Filename: tests/integration_test.rs

{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-13-fix-shared-test-code-problem/tests/integration_test.rs}}

Huomaa, että mod common; -ilmoitus on sama kuin moduuli-ilmoitus, jonka demonstroimme listauksessa 7-21. Sitten testifunktiossa voimme kutsua common::setup()-funktiota.

Integraatiotestit binääricrateille

Jos projektimme on binääricrate, joka sisältää vain src/main.rs-tiedoston eikä src/lib.rs-tiedostoa, emme voi luoda integraatiotestejä tests- hakemistoon ja tuoda src/main.rs-tiedostossa määriteltyjä funktioita näkyviin use-lauseella. Vain kirjastocratet paljastavat funktioita, joita muut cratet voivat käyttää; binääricratet on tarkoitettu ajettaviksi itsenäisesti.

Tämä on yksi syy siihen, miksi Rust-projektit, jotka tarjoavat binäärin, käyttävät suoraviivaista src/main.rs-tiedostoa, joka kutsuu logiikkaa, joka asuu src/lib.rs-tiedostossa. Tällä rakenteella integraatiotestit voivat testata kirjastocratea use-lauseella tuodakseen tärkeän toiminnallisuuden näkyviin. Jos tärkeä toiminnallisuus toimii, pieni määrä koodia src/main.rs-tiedostossa toimii myös, eikä sitä pientä koodimäärää tarvitse testata.

Yhteenveto

Rustin testausominaisuudet tarjoavat tavan määrittää, miten koodin pitäisi toimia varmistaaksesi, että se jatkaa toimimista odottamallasi tavalla, vaikka teet muutoksia. Yksikkötestit testaavat kirjaston eri osia erikseen ja voivat testata yksityisiä toteutustietoja. Integraatiotestit tarkistavat, että kirjaston monet osat toimivat yhdessä oikein, ja ne käyttävät kirjaston julkista API:a testatakseen koodia samalla tavalla kuin ulkoinen koodi käyttää sitä. Vaikka Rustin tyyppijärjestelmä ja omistajuussäännöt auttavat estämään joitakin virhetyyppejä, testit ovat silti tärkeitä vähentämään logiikkavirheitä, jotka liittyvät siihen, miten koodisi odotetaan käyttäytyvän.

Yhdistetään tässä luvussa ja aiemmissa luvuissa oppimasi tieto ja työskennellään projektin parissa!

I/O-projekti: Komentorivityökalun rakentaminen

Tässä luvussa kertaamme monia aiemmin opittuja taitoja ja tutustumme muutamiin uusiin standardikirjaston ominaisuuksiin. Rakennamme komentorivityökalun, joka käyttää tiedostoja sekä komentorivin tulo- ja lähtötietoja, jotta voimme harjoitella Rustin käsitteitä, jotka olet jo oppinut.

Rustin nopeus, turvallisuus, yksittäisen binääritiedoston tuottaminen ja monialustatuki tekevät siitä erinomaisen kielen komentorivityökalujen rakentamiseen. Projektissamme toteutamme oman version klassisesta komentorivityökalusta grep (globally search a regular expression and print).

Yksinkertaisimmassa tapauksessa grep etsii määritellystä tiedostosta määriteltyä merkkijonoa. Se saa argumentteina tiedostopolun ja etsittävän merkkijonon, lukee tiedoston, löytää rivit, jotka sisältävät annetun merkkijonon, ja tulostaa ne.

Projektin edetessä opimme tekemään komentorivityökalustamme käyttäjäystävällisemmän hyödyntämällä yleisiä terminaalin ominaisuuksia. Luemme ympäristömuuttujan arvon, jotta käyttäjä voi mukauttaa työkalun toimintaa. Lisäksi ohjaamme virheilmoitukset standardivirta-ulostuloon (stderr) tavallisen tulostusvirran (stdout) sijaan. Näin käyttäjä voi esimerkiksi ohjata onnistuneet tulokset tiedostoon samalla, kun virheilmoitukset näkyvät edelleen näytöllä.

Rust-yhteisön jäsen Andrew Gallant on jo luonut täysiverisen ja erittäin nopean grep-version nimeltä ripgrep. Meidän toteutuksemme on paljon yksinkertaisempi, mutta tämä luku antaa tarvittavan taustan, jotta voit ymmärtää oikean maailman projekteja, kuten ripgrep.

grep-projektimme yhdistää useita tähän mennessä oppimiamme konsepteja:

  • Koodin organisointi (Luku 7)
  • Vektorien ja merkkijonojen käyttö (Luku 8)
  • Virheiden käsittely (Luku 9)
  • Traitit ja eliniät oikeissa tilanteissa (Luku 10)
  • Testien kirjoittaminen (Luku 11)

Lisäksi tutustumme lyhyesti sulkeisiin (closures), iteraattoreihin ja trait-olioihin, joita käsitellään yksityiskohtaisemmin Luvuissa 13 ja 18.

Komentoriviargumenttien hyväksyminen

Komentoriviargumenttien vastaanottaminen

Luodaan uusi projekti komennolla cargo new, kuten aina. Nimeämme projektimme minigrep erottamaan sen järjestelmässäsi mahdollisesti jo olevasta grep-työkalusta.

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

Ensimmäinen tehtävä on saada minigrep vastaanottamaan kaksi komentoriviargumenttia: tiedostopolku ja etsittävä merkkijono. Haluamme pystyä ajamaan ohjelman komennolla:

$ cargo run -- hakusana esimerkkitiedosto.txt

Tällä hetkellä cargo new -komennon luoma ohjelma ei osaa käsitellä argumentteja. On olemassa valmiita kirjastoja crates.io-palvelussa, jotka helpottavat komentoriviargumenttien käsittelyä, mutta koska olemme vasta oppimassa tätä konseptia, toteutamme ominaisuuden itse.

Argumenttien lukeminen

Jotta minigrep voi lukea sille annetut komentoriviargumentit, tarvitsemme Rustin standardikirjaston std::env::args-funktion. Tämä funktio palauttaa iteraattorin, joka sisältää komentoriviargumentit.

Iteraattorit tuottavat arvojoukon, ja voimme käyttää collect-metodia kerätäksemme ne kokoelmaan, kuten vektoriin.

Seuraava koodi (Listing 12-1) mahdollistaa minigrep-ohjelman lukemaan kaikki komentoriviargumentit ja tallentamaan ne vektoriin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-01/src/main.rs}}
}
Listing 12-1: Komentoriviargumenttien kerääminen vektoriin ja tulostaminen

Ensimmäisenä tuomme std::env-moduulin käyttöön use-lauseella, jotta voimme käyttää sen args-funktiota. Koska std::env::args sijaitsee kahdessa sisäkkäisessä moduulissa, on parempi tuoda koko std::env käyttöön kuin pelkkä args. Tämä vähentää epäselvyyksiä ja mahdollistaa muidenkin std::env-moduulin funktioiden käytön.

args-funktion ja virheellisen Unicoden käsittely

Huomaa, että std::env::args-kutsu kaatuu, jos jokin argumentti sisältää virheellistä Unicodea. Jos ohjelmasi tarvitsee käsitellä argumentteja, jotka eivät ole kelvollista Unicodea, käytä std::env::args_os. Tämä funktio palauttaa iteraattorin, joka tuottaa OsString-arvoja String-arvojen sijaan.

main-funktion ensimmäisellä rivillä kutsumme env::args-funktiota ja käytämme collect-metodia muuttaaksemme iteraattorin vektoriksi. Koska collect voi luoda monenlaisia kokoelmia, meidän on annettava args-muuttujalle eksplisiittinen tyyppi (Vec<String>), jotta Rust tietää, minkä tyyppinen kokoelma halutaan.

Lopuksi tulostamme vektorin dbg!-makrolla. Kokeillaan ohjelman suorittamista ensin ilman argumentteja ja sitten kahdella argumentilla:

{{#include ../listings/ch12-an-io-project/listing-12-01/output.txt}}
{{#include ../listings/ch12-an-io-project/output-only-01-with-args/output.txt}}

Huomaa, että ensimmäinen vektorin arvo on "target/debug/minigrep", joka on ohjelmamme binäärin nimi. Tämä vastaa C-kielessä olevaa argv[0]-arvoa ja antaa ohjelmille mahdollisuuden käyttää omaa nimeään esimerkiksi käyttöohjeiden tulostamiseen. Tässä luvussa emme kuitenkaan tarvitse sitä, joten voimme jättää sen huomiotta.

Argumenttien tallentaminen muuttujiin

Nyt ohjelma pystyy vastaanottamaan komentoriviargumentteja. Seuraavaksi tallennamme nämä argumentit muuttujiin, jotta voimme käyttää niitä ohjelman muissa osissa. Tämä tapahtuu seuraavassa koodinpätkässä (Listing 12-2).

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-02/src/main.rs}}
Listing 12-2: Muuttujien luominen hakusanan ja tiedostopolun tallentamiseen

Koska ensimmäinen arvo (args[0]) on ohjelman nimi, varsinainen hakusana alkaa indeksistä 1 ja tiedostopolku indeksistä 2. Määritämme siis muuttujan query viittaamaan ensimmäiseen argumenttiin ja file_path-muuttujan toiseen argumenttiin.

Tulostamme nämä arvot tilapäisesti varmistaaksemme, että ne ovat oikein. Kokeillaan ohjelman suorittamista seuraavilla argumenteilla:

{{#include ../listings/ch12-an-io-project/listing-12-02/output.txt}}

Hienoa, ohjelma toimii! Hakusanan ja tiedostopolun arvot tallennetaan oikeisiin muuttujiin. Myöhemmin lisäämme virheenkäsittelyä tilanteisiin, joissa käyttäjä ei anna argumentteja. Toistaiseksi jatkamme tiedostojen lukemisen lisäämisellä ohjelmaamme.

Tiedoston lukeminen

Tiedoston lukeminen

Seuraavaksi lisätään toiminnallisuus, joka lukee file_path-argumentilla määritellyn tiedoston. Tarvitsemme ensin testitiedoston, jolla voimme kokeilla toimintoa. Käytämme tiedostoa, jossa on pieni määrä tekstiä useilla riveillä ja joitain toistuvia sanoja. Listing 12-3 sisältää Emily Dickinsonin runon, joka sopii hyvin testitapaukseksi!

Luo projektisi juuritason hakemistoon tiedosto nimeltä poem.txt ja kirjoita siihen runo “I’m Nobody! Who are you?”.

Filename: poem.txt
{{#include ../listings/ch12-an-io-project/listing-12-03/poem.txt}}
Listing 12-3: Emily Dickinsonin runo toimii hyvänä testiaineistona.

Kun teksti on lisätty tiedostoon, muokkaa src/main.rs-tiedostoa ja lisää siihen koodi, joka lukee tiedoston sisällön, kuten Listing 12-4 näyttää.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/src/main.rs:here}}
Listing 12-4: Toisen argumentin määrittämän tiedoston lukeminen

Ensimmäiseksi tuomme standardikirjastosta tarvittavan osan use-lauseella: tarvitsemme std::fs-moduulin tiedostojen käsittelyä varten.

main-funktiossa uusi fs::read_to_string-kutsu ottaa file_path-muuttujan, avaa tiedoston ja palauttaa std::io::Result<String>-tyyppisen arvon, joka sisältää tiedoston sisällön.

Lisäämme jälleen väliaikaisen println!-lauseen, joka tulostaa contents-muuttujan arvon tiedoston lukemisen jälkeen, jotta voimme tarkistaa, että ohjelma toimii odotetusti.

Suoritetaan ohjelma antamalla mikä tahansa merkkijono ensimmäiseksi komentoriviargumentiksi (koska emme ole vielä toteuttaneet hakutoimintoa) ja poem.txt-tiedosto toiseksi argumentiksi:

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/output.txt}}

Hienoa! Koodi luki tiedoston ja tulosti sen sisällön. Mutta tässä on muutamia ongelmia. Tällä hetkellä main-funktiolla on useita vastuita: yleensä funktiot ovat selkeämpiä ja helpompia ylläpitää, jos ne hoitavat vain yhden asian kerrallaan. Toinen ongelma on, että emme käsittele virheitä niin hyvin kuin voisimme. Ohjelma on vielä pieni, joten nämä puutteet eivät ole suuria ongelmia, mutta kun ohjelma kasvaa, niiden korjaaminen siististi vaikeutuu.

On hyvä käytäntö aloittaa refaktorointi varhaisessa vaiheessa ohjelmistokehitystä, koska pienempi määrä koodia on helpompi jäsentää uudelleen. Teemme sen seuraavaksi.

Refaktorointi: parempi modulariteetti ja virheenkäsittely

Refaktorointi modulaarisuuden ja virheenkäsittelyn parantamiseksi

Parantaaksemme ohjelmaamme korjaamme neljä ongelmaa, jotka liittyvät ohjelman rakenteeseen ja siihen, miten se käsittelee mahdollisia virheitä. Ensinnäkin main-funktiomme suorittaa nyt kaksi tehtävää: se jäsentää argumentit ja lukee tiedostoja. Ohjelmamme kasvaessa main-funktion käsittelemien erillisten tehtävien määrä kasvaa. Kun funktio saa vastuita, siitä tulee vaikeampi ymmärtää, vaikeampi testata ja vaikeampi muuttaa rikkomatta jotakin sen osista. On parasta erottaa toiminnallisuus niin, että jokainen funktio on vastuussa yhdestä tehtävästä.

Tämä ongelma liittyy myös toiseen ongelmaan: Vaikka query ja file_path ovat ohjelmamme konfiguraatiomuuttujia, muuttujat kuten contents käytetään ohjelman logiikan suorittamiseen. Mitä pidempi main tulee, sitä enemmän muuttujia meidän täytyy tuoda näkyvyysalueelle; mitä enemmän muuttujia on näkyvyysalueella, sitä vaikeampi on seurata kunkin tarkoitusta. On parasta ryhmitellä konfiguraatiomuuttujat yhteen structiin selventämään niiden tarkoitusta.

Kolmas ongelma on, että olemme käyttäneet expect-metodia tulostaaksemme virheviestin, kun tiedoston lukeminen epäonnistuu, mutta virheviesti tulostaa vain Should have been able to read the file. Tiedoston lukeminen voi epäonnistua monella tavalla: Esimerkiksi tiedosto voi puuttua tai meillä ei ehkä ole oikeutta avata sitä. Tällä hetkellä tulostaisimme saman virheviestin kaikissa tilanteissa, mikä ei antaisi käyttäjälle mitään tietoa!

Neljäs ongelma on, että käytämme expect-metodia virheen käsittelyyn, ja jos käyttäjä ajaa ohjelmamme määrittämättä tarpeeksi argumentteja, hän saa Rustilta index out of bounds -virheen, joka ei selitä ongelmaa selkeästi. Olisi parasta, jos kaikki virheenkäsittelykoodi olisi yhdessä paikassa, jotta tulevat ylläpitäjät voisivat konsultoida vain yhtä paikkaa, jos virheenkäsittelylogiikkaa tarvitsee muuttaa. Kaiken virheenkäsittelykoodin pitäminen yhdessä paikassa varmistaa myös, että tulostamme viestejä, jotka ovat merkityksellisiä loppukäyttäjillemme.

Korjataan nämä neljä ongelmaa refaktoroimalla projektimme.

Huolenaiheiden erottaminen binääriprojekteissa

Useiden tehtävien vastuun jakaminen main-funktiolle on yleinen organisointiongelma monissa binääriprojekteissa. Siksi monet Rust-ohjelmoijat pitävät hyödyllisenä jakaa binääriohjelman erilliset huolenaiheet, kun main-funktio alkaa kasvaa suureksi. Tämä prosessi sisältää seuraavat vaiheet:

  • Jaa ohjelmasi main.rs- ja lib.rs-tiedostoihin ja siirrä ohjelmasi logiikka lib.rs-tiedostoon.
  • Niin kauan kuin komentorivin jäsentämislogiikka on pieni, se voi pysyä main-funktiossa.
  • Kun komentorivin jäsentämislogiikka alkaa monimutkaistua, erota se main- funktiosta muihin funktioihin tai tyyppeihin.

Tämän prosessin jälkeen main-funktiossa jäljellä olevat vastuut tulisi rajoittaa seuraaviin:

  • Komentorivin jäsentämislogiikan kutsuminen argumenttiarvoilla
  • Muun konfiguraation asettaminen
  • run-funktion kutsuminen lib.rs-tiedostossa
  • Virheen käsittely, jos run palauttaa virheen

Tämä kuvio koskee huolenaiheiden erottamista: main.rs hoitaa ohjelman ajamisen ja lib.rs hoitaa kaiken tehtävän logiikan. Koska et voi testata main-funktiota suoraan, tämä rakenne antaa sinun testata kaiken ohjelmasi logiikan siirtämällä sen pois main-funktiosta. main-funktiossa jäljelle jäävä koodi on tarpeeksi pieni sen oikeellisuuden varmistamiseksi lukemalla. Työstetään ohjelmaamme seuraamalla tätä prosessia.

Argumenttijäsentimen erottaminen

Erotamme argumenttien jäsentämiseen liittyvän toiminnallisuuden funktioon, jonka main kutsuu. Listaus 12-5 näyttää uuden main-funktion alun, joka kutsuu uutta parse_config-funktiota, jonka määrittelemme src/main.rs-tiedostossa.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-05/src/main.rs:here}}
Listing 12-5: parse_config-funktion erottaminen main-funktiosta

Keräämme edelleen komentoriviargumentit vektoriin, mutta sen sijaan, että määrittäisimme indeksin 1 argumenttiarvon muuttujalle query ja indeksin 2 argumenttiarvon muuttujalle file_path main-funktiossa, välitämme koko vektorin parse_config-funktiolle. parse_config-funktio sisältää logiikan, joka määrittää, mikä argumentti menee mihinkin muuttujaan, ja palauttaa arvot takaisin main-funktiolle. Luomme edelleen query- ja file_path-muuttujat main-funktiossa, mutta main-funktiolla ei ole enää vastuuta määrittää, miten komentoriviargumentit ja muuttujat vastaavat toisiaan.

Tämä refaktorointi saattaa vaikuttaa liialliselta pienelle ohjelmallemme, mutta refaktoroimme pienin, inkrementaalisin askelin. Tämän muutoksen jälkeen aja ohjelma uudelleen varmistaaksesi, että argumenttien jäsentäminen toimii edelleen. On hyvä tarkistaa edistyminen usein auttaaksesi tunnistamaan ongelmien syyn, kun ne ilmenevät.

Konfiguraatioarvojen ryhmittely

Voimme tehdä vielä yhden pienen askeleen parantaaksemme parse_config-funktiota edelleen. Tällä hetkellä palautamme monikon, mutta sitten hajotamme sen heti uudelleen yksittäisiin osiin. Tämä on merkki siitä, että meillä ei ehkä ole vielä oikeaa abstraktiota.

Toinen indikaattori, joka osoittaa parannettavaa tilaa, on parse_config-funktion config-osa, joka viittaa siihen, että kaksi palauttamaamme arvoa liittyvät toisiinsa ja ovat molemmat osa yhtä konfiguraatioarvoa. Emme tällä hetkellä välittäne tätä merkitystä datan rakenteessa muuten kuin ryhmittelemällä kaksi arvoa monikkoon; sijoitamme sen sijaan kaksi arvoa yhteen structiin ja annamme kullekin structin kentälle merkityksellisen nimen. Näin tuleville tämän koodin ylläpitäjille on helpompi ymmärtää, miten eri arvot liittyvät toisiinsa ja mikä niiden tarkoitus on.

Listaus 12-6 näyttää parannukset parse_config-funktioon.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-06/src/main.rs:here}}
Listing 12-6: parse_config-funktion refaktorointi palauttamaan Config-structin instanssi

Olemme lisänneet Config-nimisen structin, jolla on kentät nimeltä query ja file_path. parse_config-funktion allekirjoitus ilmaisee nyt, että se palauttaa Config-arvon. parse_config-funktion rungossa, jossa aiemmin palautimme merkkijonoviipaleita, jotka viittaavat String-arvoihin args-vektorissa, määrittelemme nyt Config-structin sisältämään omistettuja String-arvoja. args-muuttuja main-funktiossa omistaa argumenttiarvot ja antaa vain parse_config-funktion lainata niitä, mikä tarkoittaa, että rikkoisimme Rustin lainaussääntöjä, jos Config yrittäisi ottaa args-vektorin arvojen omistajuuden.

On useita tapoja hallita String-dataa; helpoin, vaikkakin hieman tehoton tapa on kutsua clone-metodia arvoille. Tämä tekee täyden kopion datasta Config- instanssin omistettavaksi, mikä vie enemmän aikaa ja muistia kuin merkkijonodatan viittauksen säilyttäminen. Datan kloonaaminen tekee kuitenkin koodistamme hyvin suoraviivaisen, koska emme tarvitse hallita viittausten eliniöitä; tässä tilanteessa pienen suorituskyvyn uhraaminen yksinkertaisuuden saavuttamiseksi on kannattava kompromissi.

clone-metodin kompromissit

Monilla Rustaceaneilla on taipumus välttää clone-metodin käyttöä omistajuusongelmien korjaamiseen sen ajonaikaisen kustannuksen vuoksi. Luvussa 13 opit käyttämään tehokkaampia menetelmiä tämän tyyppisissä tilanteissa. Mutta toistaiseksi on ok kopioida muutama merkkijono jatkaaksesi edistymistä, koska teet nämä kopiot vain kerran ja tiedostopolku- ja hakumerkkijonosi ovat hyvin pieniä. On parempi olla hieman tehoton mutta toimiva ohjelma kuin yrittää hyperoptimoida koodia ensimmäisellä läpimenolla. Kun saat enemmän kokemusta Rustista, on helpompaa aloittaa tehokkaimmalla ratkaisulla, mutta toistaiseksi on täysin hyväksyttävää kutsua clone.

Olemme päivittäneet main-funktion niin, että se asettaa parse_config-funktion palauttaman Config-instanssin muuttujaan nimeltä config, ja olemme päivittäneet koodin, joka aiemmin käytti erillisiä query- ja file_path-muuttujia, käyttämään sen sijaan Config-structin kenttiä.

Nyt koodimme välittää selkeämmin, että query ja file_path liittyvät toisiinsa ja että niiden tarkoitus on määrittää, miten ohjelma toimii. Kaikki koodi, joka käyttää näitä arvoja, tietää etsiä ne config-instanssista kentistä, jotka on nimetty niiden tarkoituksen mukaan.

Config-structin konstruktorin luominen

Tähän asti olemme erottaneet komentoriviargumenttien jäsentämiseen liittyvän logiikan main-funktiosta ja sijoittaneet sen parse_config-funktioon. Tämä auttoi meitä näkemään, että query- ja file_path-arvot liittyivät toisiinsa, ja tämän suhteen tulisi näkyä koodissamme. Lisäsimme sitten Config-structin nimeämään query- ja file_path-arvojen liittyvän tarkoituksen ja pystyäksemme palauttamaan arvojen nimet structin kenttien niminä parse_config-funktiosta.

Nyt kun parse_config-funktion tarkoitus on luoda Config-instanssi, voimme muuttaa parse_config-funktion tavallisesta funktiosta new-nimiseksi funktioksi, joka liittyy Config-structiin. Tämä muutos tekee koodista idiomaattisempaa. Voimme luoda tyyppien instansseja standardikirjastossa, kuten String, kutsumalla String::new. Vastaavasti muuttamalla parse_config-funktion new-funktioksi, joka liittyy Config-structiin, voimme luoda Config-instansseja kutsumalla Config::new. Listaus 12-7 näyttää tarvittavat muutokset.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-07/src/main.rs:here}}
Listing 12-7: parse_config-funktion muuttaminen Config::new-funktioksi

Olemme päivittäneet main-funktion niin, että se kutsuu parse_config-funktion sijaan Config::new-funktiota. Olemme muuttaneet parse_config-funktion nimen new:ksi ja siirtäneet sen impl-lohkoon, joka liittää new-funktion Config- structiin. Kokeile kääntää tämä koodi uudelleen varmistaaksesi, että se toimii.

Virheenkäsittelyn korjaaminen

Työstetään nyt virheenkäsittelyn korjaamista. Muista, että yrittäminen käyttää args-vektorin arvoja indeksissä 1 tai 2 aiheuttaa ohjelman paniikin, jos vektori sisältää alle kolme kohdetta. Kokeile ajaa ohjelma ilman argumentteja; se näyttää tältä:

{{#include ../listings/ch12-an-io-project/listing-12-07/output.txt}}

Rivi index out of bounds: the len is 1 but the index is 1 on ohjelmoijille tarkoitettu virheilmoitus. Se ei auta loppukäyttäjiämme ymmärtämään, mitä heidän pitäisi tehdä sen sijaan. Korjataan se nyt.

Virheviestin parantaminen

Listauksessa 12-8 lisäämme tarkistuksen new-funktioon, joka varmistaa, että viipale on tarpeeksi pitkä ennen kuin käytämme indeksejä 1 ja 2. Jos viipale ei ole tarpeeksi pitkä, ohjelma paniikkiutuu ja näyttää paremman virheviestin.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-08/src/main.rs:here}}
Listing 12-8: Tarkistuksen lisääminen argumenttien määrälle

Tämä koodi on samanlainen kuin Guess::new-funktio, jonka kirjoitimme listauksessa 9-13, jossa kutsuimme panic!-makroa, kun value-argumentti oli kelvollisten arvojen alueen ulkopuolella. Sen sijaan, että tarkistaisimme arvojen alueen tässä, tarkistamme, että args-vektorin pituus on vähintään 3, ja loput funktiosta voivat toimia olettaen, että tämä ehto on täyttynyt. Jos args- vektorissa on alle kolme kohdetta, tämä ehto on tosi, ja kutsumme panic!- makroa lopettaaksemme ohjelman välittömästi.

Näillä muutamalla lisärivillä new-funktiossa ajetaan ohjelma uudelleen ilman argumentteja nähdäksemme, miltä virhe näyttää nyt:

{{#include ../listings/ch12-an-io-project/listing-12-08/output.txt}}

Tämä tuloste on parempi: Meillä on nyt järkevä virheilmoitus. Meillä on kuitenkin myös ylimääräistä tietoa, jota emme halua antaa käyttäjillemme. Ehkä listauksessa 9-13 käyttämämme tekniikka ei ole paras käytettäväksi tässä: panic!-kutsu on sopivampi ohjelmointiongelmaan kuin käyttöongelmaan, kuten käsiteltiin luvussa 9. Sen sijaan käytämme toista luvussa 9 oppimaamme tekniikkaa — palautamme Result-arvon, joka ilmaisee joko onnistumisen tai virheen.

Result-arvon palauttaminen panic!-kutsun sijaan

Voimme sen sijaan palauttaa Result-arvon, joka sisältää Config-instanssin onnistumistapauksessa ja kuvaa ongelman virhetapauksessa. Muutamme myös funktion nimen new:stä build:ksi, koska monet ohjelmoijat odottavat new-funktioiden eivän koskaan epäonnistuvan. Kun Config::build kommunikoi main-funktion kanssa, voimme käyttää Result-tyyppiä ilmaisemaan, että ongelma oli. Sitten voimme muuttaa main-funktion muuntamaan Err-variantin käytännöllisemmäksi virheeksi käyttäjillemme ilman ympäröivää tekstiä thread 'main' ja RUST_BACKTRACE panic!-kutsun aiheuttamasta.

Listaus 12-9 näyttää muutokset, jotka meidän täytyy tehdä funktion palautusarvoon, jota kutsumme nyt Config::build, ja funktion runkoon, joka tarvitaan Result- arvon palauttamiseen. Huomaa, että tämä ei käänny ennen kuin päivitämme myös main-funktion, minkä teemme seuraavassa listauksessa.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-09/src/main.rs:here}}
Listing 12-9: Result-arvon palauttaminen Config::build-funktiosta

build-funktiomme palauttaa Result-arvon, jossa on Config-instanssi onnistumistapauksessa ja merkkijonoliteraali virhetapauksessa. Virhearvomme ovat aina merkkijonoliteraaleja, joilla on 'static-elinikä.

Olemme tehneet kaksi muutosta funktion rungossa: Sen sijaan, että kutsuisimme panic!-makroa, kun käyttäjä ei anna tarpeeksi argumentteja, palautamme nyt Err-arvon, ja olemme käärittäneet Config-palautusarvon Ok-arvoon. Nämä muutokset saavat funktion vastaamaan uutta tyyppiallekirjoitustaan.

Err-arvon palauttaminen Config::build-funktiosta antaa main-funktion käsitellä build-funktion palauttaman Result-arvon ja lopettaa prosessin siistimmin virhetapauksessa.

Config::build-funktion kutsuminen ja virheiden käsittely

Käsitelläksemme virhetapauksen ja tulostaaksemme käyttäjäystävällisen viestin, meidän täytyy päivittää main käsittelemään Config::build-funktion palauttama Result, kuten listauksessa 12-10. Otamme myös vastuun komentorivityökalun lopettamisesta nollasta poikkeavalla virhekoodilla pois panic!-makrosta ja toteutamme sen käsin. Nollasta poikkeava poistumistila on käytäntö, jolla signaaloidaan prosessille, joka kutsui ohjelmaamme, että ohjelma lopetti virhetilassa.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-10/src/main.rs:here}}
Listing 12-10: Poistuminen virhekoodilla, jos Config-instanssin rakentaminen epäonnistuu

Tässä listauksessa olemme käyttäneet metodia, jota emme ole käsitelleet yksityiskohtaisesti: unwrap_or_else, joka on määritelty Result<T, E>-tyypille standardikirjastossa. unwrap_or_else-metodin käyttö antaa meille määritellä mukautetun, ei-panic!-virheenkäsittelyn. Jos Result on Ok-arvo, tämän metodin käyttäytyminen on samanlainen kuin unwrap: Se palauttaa Ok-arvon sisältämän sisäisen arvon. Jos arvo on kuitenkin Err-arvo, tämä metodi kutsuu sulkeuman koodia, joka on anonyymi funktio, jonka määrittelemme ja välitämme argumenttina unwrap_or_else-metodille. Käsittelemme sulkeumia yksityiskohtaisemmin luvussa 13. Toistaiseksi sinun tarvitsee vain tietää, että unwrap_or_else välittää Err-arvon sisäisen arvon, joka tässä tapauksessa on staattinen merkkijono "not enough arguments", jonka lisäsimme listauksessa 12-9, sulkeumaamme argumentissa err, joka esiintyy pystyviivojen välissä. Sulkeuman koodi voi sitten käyttää err-arvoa suorittaessaan.

Olemme lisänneet uuden use-rivin tuodaksemme process-moduulin standardikirjastosta näkyviin. Sulkeuman koodi, joka suoritetaan virhetapauksessa, on vain kaksi riviä: Tulostamme err-arvon ja kutsumme sitten process::exit. process::exit- funktio pysäyttää ohjelman välittömästi ja palauttaa numeron, joka annettiin poistumistilakoodina. Tämä on samanlaista kuin listauksessa 12-8 käyttämämme panic!-pohjainen käsittely, mutta emme enää saa kaikkea ylimääräistä tulostetta. Kokeillaan:

{{#include ../listings/ch12-an-io-project/listing-12-10/output.txt}}

Hienoa! Tämä tuloste on paljon ystävällisempi käyttäjillemme.

Logiikan erottaminen main-funktiosta

Nyt kun olemme lopettaneet konfiguraation jäsentämisen refaktoroinnin, käännymme ohjelman logiikan pariin. Kuten totesimme osiossa ”Huolenaiheiden erottaminen binääriprojekteissa”, erotamme run-nimisen funktion, joka sisältää kaiken logiikan, joka on tällä hetkellä main-funktiossa eikä liity konfiguraation asettamiseen tai virheiden käsittelyyn. Kun olemme valmiit, main-funktio on tiivis ja helppo varmistaa tarkastelemalla, ja voimme kirjoittaa testejä kaikelle muulle logiikalle.

Listaus 12-11 näyttää pienen, inkrementaalisen parannuksen run-funktion erottamisesta.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-11/src/main.rs:here}}
Listing 12-11: run-funktion erottaminen, joka sisältää loput ohjelman logiikasta

run-funktio sisältää nyt kaiken jäljellä olevan logiikan main-funktiosta alkaen tiedoston lukemisesta. run-funktio ottaa Config-instanssin argumenttina.

Virheiden palauttaminen run-funktiosta

Kun jäljellä oleva ohjelman logiikka on erotettu run-funktioon, voimme parantaa virheenkäsittelyä, kuten teimme Config::build-funktiossa listauksessa 12-9. Sen sijaan, että sallisimme ohjelman paniikin kutsumalla expect-metodia, run-funktio palauttaa Result<T, E>-arvon, kun jokin menee pieleen. Tämä antaa meille mahdollisuuden koota virheenkäsittelylogiikkaa edelleen main- funktioon käyttäjäystävällisellä tavalla. Listaus 12-12 näyttää muutokset, jotka meidän täytyy tehdä run-funktion allekirjoitukseen ja runkoon.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-12/src/main.rs:here}}
Listing 12-12: run-funktion muuttaminen palauttamaan Result

Olemme tehneet kolme merkittävää muutosta. Ensinnäkin muutimme run-funktion palautustyypiksi Result<(), Box<dyn Error>>. Tämä funktio palautti aiemmin yksikkötyypin (), ja säilytämme sen Ok-tapauksen palautusarvona.

Virhetyypiksi käytimme trait-oliota Box<dyn Error> (ja toimme std::error::Error- moduulin näkyviin use-lauseella tiedoston alussa). Käsittelemme trait-olioita luvussa 18. Toistaiseksi tiedä vain, että Box<dyn Error> tarkoittaa, että funktio palauttaa tyypin, joka toteuttaa Error-traitin, mutta emme tarvitse määrittää, mikä tietty tyyppi palautusarvo on. Tämä antaa meille joustavuutta palauttaa virhearvoja, jotka voivat olla eri tyyppejä eri virhetapauksissa. dyn-avainsana on lyhenne sanasta dynamic.

Toiseksi olemme poistaneet expect-kutsun ?-operaattorin hyväksi, kuten puhuimme luvussa 9. Sen sijaan, että panic! virheessä, ? palauttaa virhearvon nykyisestä funktiosta kutsujan käsiteltäväksi.

Kolmanneksi run-funktio palauttaa nyt Ok-arvon onnistumistapauksessa. Olemme ilmoittaneet run-funktion onnistumistyypiksi () allekirjoituksessa, mikä tarkoittaa, että meidän täytyy kääriä yksikkötyypin arvo Ok-arvoon. Tämä Ok(())-syntaksi saattaa aluksi näyttää hieman oudolta. Mutta ()-tyypin käyttö tällä tavalla on idiomaattinen tapa ilmaista, että kutsumme run-funktiota vain sen sivuvaikutuksia varten; se ei palauta arvoa, jota tarvitsemme.

Kun ajat tämän koodin, se kääntyy mutta näyttää varoituksen:

{{#include ../listings/ch12-an-io-project/listing-12-12/output.txt}}

Rust kertoo, että koodimme ohitti Result-arvon ja Result-arvo saattaa ilmaista, että virhe tapahtui. Mutta emme tarkista, tapahtuiko virhe, ja kääntäjä muistuttaa, että meillä oli todennäköisesti tarkoitus olla virheenkäsittelykoodia tässä! Korjataan tämä ongelma nyt.

run-funktion palauttamien virheiden käsittely main-funktiossa

Tarkistamme virheet ja käsittelemme ne tekniikalla, joka on samanlainen kuin Config::build-funktion kanssa listauksessa 12-10, mutta pienellä erolla:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch12-an-io-project/no-listing-01-handling-errors-in-main/src/main.rs:here}}

Käytämme if let -lausetta unwrap_or_else-metodin sijaan tarkistaaksemme, palauttaako run Err-arvon, ja kutsumme process::exit(1), jos se palauttaa. run-funktio ei palauta arvoa, jota haluamme unwrap-metodilla purkaa samalla tavalla kuin Config::build palauttaa Config-instanssin. Koska run palauttaa () onnistumistapauksessa, välitämme vain virheen havaitsemisesta, joten emme tarvitse unwrap_or_else-metodia palauttamaan purettua arvoa, joka olisi vain ().

if let - ja unwrap_or_else-funktioiden rungot ovat samat molemmissa tapauksissa: Tulostamme virheen ja poistumme.

Koodin jakaminen kirjastocrateen

minigrep-projektimme näyttää hyvältä tähän asti! Jaamme nyt src/main.rs- tiedoston ja siirrämme osan koodista src/lib.rs-tiedostoon. Näin voimme testata koodia ja meillä on src/main.rs-tiedosto, jolla on vähemmän vastuita.

Määritellään tekstin hakemiseen liittyvä koodi src/lib.rs-tiedostossa src/main.rs-tiedoston sijaan, jolloin voimme (tai kuka tahansa minigrep- kirjastomme käyttäjä) kutsua hakufunktiota useammissa konteksteissa kuin minigrep-binäärimme.

Ensin määritellään search-funktion allekirjoitus src/lib.rs-tiedostossa, kuten listauksessa 12-13, rungolla, joka kutsuu unimplemented!-makroa. Selitämme allekirjoituksen yksityiskohtaisemmin, kun täytämme toteutuksen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-13/src/lib.rs}}
Listing 12-13: search-funktion määrittely src/lib.rs-tiedostossa

Olemme käyttäneet pub-avainsanaa funktiomäärittelyssä merkitsemään search- funktion osaksi kirjastocratemme julkista API:a. Meillä on nyt kirjastocrate, jota voimme käyttää binääricratestamme ja jota voimme testata!

Meidän täytyy nyt tuoda src/lib.rs-tiedostossa määritelty koodi binääricraten näkyvyysalueelle src/main.rs-tiedostossa ja kutsua sitä, kuten listauksessa 12-14.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-14/src/main.rs:here}}
Listing 12-14: minigrep-kirjastocraten search-funktion käyttö src/main.rs-tiedostossa

Lisäämme use minigrep::search -rivin tuodaksemme search-funktion kirjastocratesta binääricraten näkyvyysalueelle. Sitten run-funktiossa tiedoston sisällön tulostamisen sijaan kutsumme search-funktiota ja välitämme config.query- arvon ja contents-arvon argumentteina. Sitten run käyttää for-silmukkaa tulostaakseen jokaisen search-funktion palauttaman rivin, joka vastasi hakua. Tämä on myös hyvä hetki poistaa println!-kutsut main-funktiosta, jotka näyttivät haun ja tiedostopolun, jotta ohjelmamme tulostaa vain hakutulokset (jos virheitä ei tapahdu).

Huomaa, että hakufunktio kerää kaikki tulokset vektoriin, jonka se palauttaa ennen kuin mitään tulostusta tapahtuu. Tämä toteutus voi olla hidas näyttämään tuloksia suurten tiedostojen haussa, koska tuloksia ei tulosteta niiden löytyessä; käsittelemme mahdollisen korjauksen iteraattoreiden avulla luvussa 13.

Huh! Sitä oli paljon työtä, mutta olemme asettaneet itsellemme pohjan menestykseen tulevaisuudessa. Nyt on paljon helpompaa käsitellä virheitä, ja olemme tehneet koodista modulaarisempaa. Lähes kaikki työmme tehdään tästä eteenpäin src/lib.rs- tiedostossa.

Hyödynnetään tätä uutta modulaarisuutta tekemällä jotain, mikä olisi ollut vaikeaa vanhalla koodilla mutta on helppoa uudella koodilla: Kirjoitamme testejä!

Kirjaston toiminnallisuuden kehittäminen testivetoista kehitystä käyttäen

Toiminnallisuuden lisääminen testivetoisella kehityksellä

Nyt kun hakulogiikka on src/lib.rs-tiedostossa erillään main-funktiosta, on paljon helpompaa kirjoittaa testejä koodimme ydintoiminnallisuudelle. Voimme kutsua funktioita suoraan eri argumenteilla ja tarkistaa palautusarvot kutsumatta binääriämme komentoriviltä.

Tässä osiossa lisäämme hakulogiikan minigrep-ohjelmaan käyttäen testivetoista kehitysprosessia (test-driven development, TDD) seuraavilla vaiheilla:

  1. Kirjoita testi, joka epäonnistuu, ja aja se varmistaaksesi, että se epäonnistuu odottamastasi syystä.
  2. Kirjoita tai muokkaa juuri tarpeeksi koodia, jotta uusi testi läpäisee.
  3. Refaktoroi juuri lisäämäsi tai muuttamasi koodi ja varmista, että testit jatkavat läpäisemistä.
  4. Toista vaiheesta 1!

Vaikka se on vain yksi monista tavoista kirjoittaa ohjelmistoa, TDD voi auttaa ohjaamaan koodin suunnittelua. Testin kirjoittaminen ennen koodia, joka saa testin läpäisemään, auttaa ylläpitämään korkeaa testikattavuutta koko prosessin ajan.

Testivetoisesti toteutamme toiminnallisuuden, joka todella etsii hakumerkkijonoa tiedoston sisällöstä ja tuottaa listan riveistä, jotka vastaavat hakua. Lisäämme tämän toiminnallisuuden search-nimiseen funktioon.

Epäonnistuvan testin kirjoittaminen

src/lib.rs-tiedostossa lisäämme tests-moduulin testifunktiolla, kuten teimme luvussa 11. Testifunktio määrittää käyttäytymisen, jonka haluamme search- funktiolla olevan: Se ottaa haun ja haettavan tekstin ja palauttaa vain rivit tekstistä, jotka sisältävät haun. Listaus 12-15 näyttää tämän testin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-15/src/lib.rs:here}}
Listing 12-15: Epäonnistuvan testin luominen search-funktiolle toiminnallisuudelle, jota toivoisimme olevan

Tämä testi etsii merkkijonoa "duct". Haettava teksti on kolme riviä, joista vain yksi sisältää "duct" (huomaa, että kenoviiva avaavan lainausmerkin jälkeen kertoo Rustille olla laittamasta rivinvaihtomerkkiä tämän merkkijonoliteraalin sisällön alkuun). Varmistamme, että search-funktion palauttama arvo sisältää vain odottamamme rivin.

Jos ajamme tämän testin, se epäonnistuu tällä hetkellä, koska unimplemented!- makro paniikkiutuu viestillä ”not implemented”. TDD-periaatteiden mukaisesti otamme pienen askeleen lisäämällä juuri tarpeeksi koodia, jotta testi ei paniikkiudu funktiota kutsuttaessa määrittelemällä search-funktion palauttamaan aina tyhjän vektorin, kuten listauksessa 12-16. Sitten testin pitäisi kääntyä ja epäonnistua, koska tyhjä vektori ei vastaa vektoria, joka sisältää rivin "safe, fast, productive.".

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-16/src/lib.rs:here}}
Listing 12-16: Juuri tarpeeksi search-funktion määrittelyä, jotta sen kutsuminen ei paniikkiudu

Keskustellaan nyt, miksi meidän täytyy määrittää eksplisiittinen elinikä 'a search-funktion allekirjoituksessa ja käyttää sitä contents-argumentin ja palautusarvon kanssa. Muista luvussa 10, että elinikäparametrit määrittävät, mikä argumentin elinikä on yhdistetty palautusarvon elinikään. Tässä tapauksessa ilmaisemme, että palautetun vektorin pitäisi sisältää merkkijonoviipaleita, jotka viittaavat contents-argumentin viipaleisiin (eivät query-argumentin viipaleisiin).

Toisin sanoen kerromme Rustille, että search-funktion palauttama data elää niin kauan kuin search-funktiolle contents-argumentissa annettu data. Tämä on tärkeää! Viipaleen viittaaman datan täytyy olla kelvollista, jotta viittaus on kelvollinen; jos kääntäjä olettaisi tekevämme merkkijonoviipaleita query- argumentista contents-argumentin sijaan, se tekisi turvallisuustarkistuksensa väärin.

Jos unohdamme elinikämerkinnät ja yritämme kääntää tämän funktion, saamme tämän virheen:

{{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}}

Rust ei voi tietää, kumpaa kahdesta parametrista tarvitsemme tulosteeseen, joten meidän täytyy kertoa se eksplisiittisesti. Huomaa, että ohjeteksti ehdottaa saman elinikäparametrin määrittämistä kaikille parametreille ja tulostyypille, mikä on virheellistä! Koska contents on parametri, joka sisältää kaiken tekstimme ja haluamme palauttaa osat siitä tekstistä, jotka vastaavat, tiedämme, että vain contents tulisi yhdistää palautusarvoon elinikäsyntaksilla.

Muut ohjelmointikielet eivät vaadi argumenttien yhdistämistä palautusarvoihin allekirjoituksessa, mutta tästä käytännöstä tulee helpompaa ajan myötä. Saatat haluta verrata tätä esimerkkiä luvun 10 osion ”Viittausten validointi elinien avulla” esimerkkeihin.

Testin läpäisevän koodin kirjoittaminen

Tällä hetkellä testimme epäonnistuu, koska palautamme aina tyhjän vektorin. Korjataksemme sen ja toteuttaaksemme search-funktion, ohjelmamme täytyy seurata näitä vaiheita:

  1. Käy läpi jokainen rivi sisällöstä.
  2. Tarkista, sisältääkö rivi hakumerkkijonomme.
  3. Jos sisältää, lisää se palautettavien arvojen listaan.
  4. Jos ei sisällä, älä tee mitään.
  5. Palauta vastaavien tulosten lista.

Työstetään jokainen vaihe, alkaen rivien läpikäynnistä.

Rivien läpikäynti lines-metodilla

Rustissa on hyödyllinen metodi merkkijonojen rivi riviltä -iterointiin, sopivasti nimeltä lines, joka toimii kuten listauksessa 12-17. Huomaa, että tämä ei vielä käänny.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-17/src/lib.rs:here}}
Listing 12-17: Jokaisen rivin läpikäynti contents-parametrissa

lines-metodi palauttaa iteraattorin. Käsittelemme iteraattoreita syvällisesti luvussa 13. Mutta muista, että näit tämän iteraattorin käyttötavan listauksessa 3-5, jossa käytimme for-silmukkaa iteraattorin kanssa suorittaaksemme koodia jokaiselle kokoelman kohteelle.

Haun etsiminen jokaiselta riviltä

Seuraavaksi tarkistamme, sisältääkö nykyinen rivi hakumerkkijonomme. Onneksi merkkijonoilla on hyödyllinen metodi nimeltä contains, joka tekee tämän meille! Lisää contains-metodin kutsu search-funktioon, kuten listauksessa 12-18. Huomaa, että tämä ei vielä käänny.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-18/src/lib.rs:here}}
Listing 12-18: Toiminnallisuuden lisääminen tarkistamaan, sisältääkö rivi query-merkkijonon

Tällä hetkellä rakennamme toiminnallisuutta. Saadaksemme koodin kääntymään meidän täytyy palauttaa arvo rungosta, kuten ilmaisimme funktion allekirjoituksessa.

Vastaavien rivien tallentaminen

Viimeistelläksemme tämän funktion tarvitsemme tavan tallentaa vastaavat rivit, jotka haluamme palauttaa. Voimme tehdä muuttuvan vektorin ennen for-silmukkaa ja kutsua push-metodia tallentaaksemme line-rivin vektoriin. for-silmukan jälkeen palautamme vektorin, kuten listauksessa 12-19.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:here}}
Listing 12-19: Vastaavien rivien tallentaminen, jotta voimme palauttaa ne

Nyt search-funktion pitäisi palauttaa vain rivit, jotka sisältävät query-merkkijonon, ja testimme pitäisi läpäistä. Ajetaan testi:

{{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}}

Testimme läpäisi, joten tiedämme, että se toimii!

Tässä vaiheessa voisimme harkita refaktorointimahdollisuuksia hakufunktion toteutuksessa pitäen testit läpäisevinä säilyttääksemme saman toiminnallisuuden. Hakufunktion koodi ei ole liian huono, mutta se ei hyödynnä iteraattoreiden hyödyllisiä ominaisuuksia. Palaamme tähän esimerkkiin luvussa 13, jossa tutkimme iteraattoreita yksityiskohtaisesti, ja katsomme, miten sitä voisi parantaa.

Nyt koko ohjelman pitäisi toimia! Kokeillaan sitä, ensin sanalla, joka pitäisi palauttaa täsmälleen yksi rivi Emily Dickinsonin runosta: frog.

{{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}}

Siistiä! Kokeillaan sitten sanaa, joka vastaa useita rivejä, kuten body:

{{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}}

Ja lopuksi varmistetaan, ettei saamme rivejä, kun etsimme sanaa, jota ei ole missään runossa, kuten monomorphization:

{{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.txt}}

Erinomaista! Olemme rakentaneet oman pienen version klassisesta työkalusta ja oppineet paljon sovellusten rakentamisesta. Olemme myös oppineet hieman tiedoston syöttöä ja tulostusta, eliniöitä, testausta ja komentorivin jäsentämistä.

Viimeistelläksemme tämän projektin demonstroimme lyhyesti, miten työskennellä ympäristömuuttujien kanssa ja miten tulostaa vakiovirheeseen, molemmat hyödyllisiä kirjoitettaessa komentoriviohjelmia.

Ympäristömuuttujien käyttö

Työskentely ympäristömuuttujien kanssa

Parannamme minigrep-binääriä lisäämällä ylimääräisen ominaisuuden: kirjainkoon huomioimattoman haun vaihtoehdon, jonka käyttäjä voi kytkeä päälle ympäristömuuttujan avulla. Voisimme tehdä tästä ominaisuuden komentorivivalinnan ja vaatia käyttäjän syöttämään sen joka kerta, kun haluaa sen käyttöön, mutta tekemällä siitä ympäristömuuttujan sallimme käyttäjiemme asettaa ympäristömuuttujan kerran ja kaikki heidän hakunsa ovat kirjainkosta riippumattomia kyseisessä terminaali- istunnossa.

Epäonnistuvan testin kirjoittaminen kirjainkosta riippumattomalle haulle

Lisäämme ensin uuden search_case_insensitive-funktion minigrep-kirjastoon, jota kutsutaan, kun ympäristömuuttujalla on arvo. Jatkamme TDD-prosessia, joten ensimmäinen askel on taas kirjoittaa epäonnistuva testi. Lisäämme uuden testin uudelle search_case_insensitive-funktiolle ja nimeämme vanhan testimme uudelleen one_result:sta case_sensitive:ksi selventääksemme kahden testin eroja, kuten listauksessa 12-20.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-20/src/lib.rs:here}}
Listing 12-20: Uuden epäonnistuvan testin lisääminen lisäämällemme kirjainkosta riippumattomalle funktiolle

Huomaa, että olemme muokanneet myös vanhan testin contents-arvoa. Olemme lisänneet uuden rivin tekstillä "Duct tape." käyttäen isoa D:tä, joka ei pitäisi vastata hakua "duct", kun haemme kirjainkoon huomioivasti. Vanhan testin muuttaminen tällä tavalla auttaa varmistamaan, ettei vahingossa riko jo toteuttamaamme kirjainkoon huomioivaa hakutoiminnallisuutta. Tämän testin pitäisi läpäistä nyt ja jatkossakin läpäistä työskennellessämme kirjainkosta riippumattoman haun parissa.

Uusi testi kirjainkosta riippumattomalle haulle käyttää hakuaan "rUsT". search_case_insensitive-funktiossa, jonka olemme lisäämässä, haun "rUsT" pitäisi vastata riviä, joka sisältää "Rust:" isolla R:llä, ja riviä "Trust me." vaikka molemmat eroavat kirjainkoosta hausta. Tämä on epäonnistuva testimme, ja se ei käänny, koska emme ole vielä määritelleet search_case_insensitive- funktiota. Voit vapaasti lisätä luurangon toteutuksen, joka palauttaa aina tyhjän vektorin, samalla tavalla kuin teimme search-funktiolle listauksessa 12-16 nähdäksesi testin kääntyvän ja epäonnistuvan.

search_case_insensitive-funktion toteuttaminen

search_case_insensitive-funktio, joka näytetään listauksessa 12-21, on lähes sama kuin search-funktio. Ainoa ero on, että muutamme query-merkkijonon ja jokaisen line-rivin pieniksi kirjaimiksi, jotta riippumatta syöteargumenttien kirjainkoosta ne ovat samaa kirjainkokoa, kun tarkistamme, sisältääkö rivi haun.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-21/src/lib.rs:here}}
Listing 12-21: search_case_insensitive-funktion määrittely, joka muuttaa haun ja rivin pieniksi kirjaimiksi ennen vertailua

Ensin muutamme query-merkkijonon pieniksi kirjaimiksi ja tallennamme sen uuteen muuttujaan samalla nimellä varjostaen alkuperäisen query-arvon. query- merkkijonon to_lowercase-kutsu on tarpeen, jotta riippumatta siitä, onko käyttäjän hakusi "rust", "RUST", "Rust" vai "rUsT", käsittelemme haun kuin se olisi "rust" ja olemme kirjainkoosta riippumattomia. Vaikka to_lowercase käsittelee perus-Unicodea, se ei ole 100-prosenttisen tarkka. Jos kirjoittaisimme oikean sovelluksen, tekisimme täällä hieman enemmän työtä, mutta tämä osio koskee ympäristömuuttujia, ei Unicodea, joten jätämme sen tähän.

Huomaa, että query on nyt String eikä merkkijonoviipale, koska to_lowercase- kutsu luo uutta dataa sen sijaan, että viittaisi olemassa olevaan dataan. Sanotaan, että haku on "rUsT" esimerkkinä: Tuo merkkijonoviipale ei sisällä pientä u:ta tai t:tä käytettäväksi, joten meidän täytyy varata uusi String, joka sisältää "rust". Kun välitämme query-arvon argumenttina contains-metodille nyt, meidän täytyy lisätä etumerkki, koska contains-metodin allekirjoitus on määritelty ottamaan merkkijonoviipale.

Seuraavaksi lisäämme to_lowercase-kutsun jokaiselle line-riville muuttaaksemme kaikki merkit pieniksi kirjaimiksi. Nyt kun olemme muuttaneet line- ja query- arvot pieniksi kirjaimiksi, löydämme osumia riippumatta haun kirjainkoosta.

Katsotaan, läpäisevätkö nämä toteutukset testit:

{{#include ../listings/ch12-an-io-project/listing-12-21/output.txt}}

Hienoa! Ne läpäisivät. Kutsutaan nyt uutta search_case_insensitive-funktiota run-funktiosta. Ensin lisäämme konfiguraatiovaihtoehdon Config-structiin vaihtaaksemme kirjainkoon huomioivan ja kirjainkosta riippumattoman haun välillä. Tämän kentän lisääminen aiheuttaa kääntäjävirheitä, koska emme alusta tätä kenttää vielä missään:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/main.rs:here}}

Lisäsimme ignore_case-kentän, joka säilyttää totuusarvon. Seuraavaksi run- funktion täytyy tarkistaa ignore_case-kentän arvo ja käyttää sitä päättääkseen, kutsutaanko search- vai search_case_insensitive-funktiota, kuten listauksessa 12-22. Tämä ei vielä käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/main.rs:there}}
Listing 12-22: Joko search- tai search_case_insensitive-funktion kutsuminen config.ignore_case-arvon perusteella

Lopuksi meidän täytyy tarkistaa ympäristömuuttuja. Ympäristömuuttujien kanssa työskentelyyn liittyvät funktiot ovat env-moduulissa standardikirjastossa, joka on jo näkyvyysalueella src/main.rs-tiedoston alussa. Käytämme env- moduulin var-funktiota tarkistaaksemme, onko ympäristömuuttujalle nimeltä IGNORE_CASE asetettu jokin arvo, kuten listauksessa 12-23.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-23/src/main.rs:here}}
Listing 12-23: Minkä tahansa arvon tarkistaminen IGNORE_CASE-nimisessä ympäristömuuttujassa

Tässä luomme uuden muuttujan ignore_case. Asettaaksemme sen arvon kutsumme env::var-funktiota ja välitämme sille IGNORE_CASE-ympäristömuuttujan nimen. env::var-funktio palauttaa Result-arvon, joka on onnistunut Ok-variantti, joka sisältää ympäristömuuttujan arvon, jos ympäristömuuttuja on asetettu mihin tahansa arvoon. Se palauttaa Err-variantin, jos ympäristömuuttujaa ei ole asetettu.

Käytämme Result-arvon is_ok-metodia tarkistaaksemme, onko ympäristömuuttuja asetettu, mikä tarkoittaa, että ohjelman pitäisi tehdä kirjainkosta riippumaton haku. Jos IGNORE_CASE-ympäristömuuttujaa ei ole asetettu mihinkään, is_ok palauttaa false ja ohjelma suorittaa kirjainkoon huomioivan haun. Emme välitä ympäristömuuttujan arvosta, vain siitä, onko se asetettu vai ei, joten tarkistamme is_ok-metodilla unwrap-, expect- tai muiden Result-arvoon liittyvien metodien sijaan.

Välitämme ignore_case-muuttujan arvon Config-instanssille, jotta run- funktio voi lukea sen arvon ja päättää, kutsutaanko search_case_insensitive- vai search-funktiota, kuten toteutimme listauksessa 12-22.

Kokeillaan! Ensin ajamme ohjelmamme ilman asetettua ympäristömuuttujaa ja haulla to, joka pitäisi vastata mitä tahansa riviä, joka sisältää sanan to kaikilla pienillä kirjaimilla:

{{#include ../listings/ch12-an-io-project/listing-12-23/output.txt}}

Näyttää siltä, että se toimii edelleen! Ajetaan nyt ohjelma IGNORE_CASE- ympäristömuuttujalla asetettuna arvoon 1 mutta samalla haulla to:

$ IGNORE_CASE=1 cargo run -- to poem.txt

Jos käytät PowerShelliä, sinun täytyy asettaa ympäristömuuttuja ja ajaa ohjelma erillisinä komentoina:

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

Tämä saa IGNORE_CASE:n pysymään voimassa loppuun asti komentotulkki-istunnossasi. Sen voi poistaa Remove-Item-cmdletillä:

PS> Remove-Item Env:IGNORE_CASE

Meidän pitäisi saada rivit, jotka sisältävät to-sanoja, joissa voi olla isoja kirjaimia:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

Erinomaista, saimme myös rivejä, jotka sisältävät To-sanoja! minigrep-ohjelmamme voi nyt tehdä kirjainkosta riippumatonta hakua ympäristömuuttujan ohjaamana. Nyt tiedät, miten hallita vaihtoehtoja, jotka on asetettu joko komentoriviargumenttien tai ympäristömuuttujien avulla.

Jotkut ohjelmat sallivat argumentit ja ympäristömuuttujat samalle konfiguraatiolle. Näissä tapauksissa ohjelmat päättävät, kumpi on etusijalla. Harjoitukseksi itsenäisesti kokeile hallita kirjainkoon huomiointia joko komentoriviargumentin tai ympäristömuuttujan avulla. Päätä, pitäisikö komentoriviargumentin vai ympäristömuuttujan olla etusijalla, jos ohjelma ajetaan toisen asetettuna kirjainkoon huomioivaksi ja toisen kirjainkoon huomioimattomaksi.

std::env-moduuli sisältää paljon hyödyllisiä ominaisuuksia ympäristömuuttujien käsittelyyn: Tutustu sen dokumentaatioon nähdäksesi, mitä on saatavilla.

Virheilmoitusten kirjoittaminen standardivirheeseen standarditulosteen sijaan

Virheviestien kirjoittaminen standardivirtaan stderr

Tällä hetkellä kirjoitamme kaiken tulosteen terminaaliin käyttämällä println!-makroa. Useimmissa terminaaleissa on kuitenkin kaksi erilaista tulostusvirtaa:

  • Standardituloste (stdout), jota käytetään yleiseen tulostamiseen.
  • Standardivirta (stderr), jota käytetään virheilmoituksiin.

Tämä erottelu mahdollistaa sen, että käyttäjät voivat ohjata ohjelman onnistuneen tulostuksen tiedostoon, mutta pitää virheilmoitukset näkyvissä näytöllä.

println!-makro pystyy tulostamaan vain standarditulosteeseen, joten meidän on käytettävä jotain muuta tulostaaksemme virheilmoitukset standardivirtaan.

Tarkistetaan, mihin virheet kirjoitetaan

Ensin havainnoimme, miten minigrep tulostaa tietoa tällä hetkellä. Aiomme ohjata standarditulosteen tiedostoon mutta jättää standardivirran ohjaamatta, jotta näemme, tulostuuko virheilmoitus tiedostoon vai terminaaliin.

Komentoriviohjelmien tulisi lähettää virheilmoitukset standardivirtaan, jotta käyttäjät voivat yhä nähdä virheet, vaikka standardituloste ohjattaisiin tiedostoon. Ohjelmamme ei vielä toimi näin, vaan tallentaa virheilmoituksen tiedostoon!

Havainnollistetaan tätä suorittamalla ohjelma ja ohjaamalla standardituloste tiedostoon output.txt. Emme anna komentoriviargumentteja, mikä tuottaa virheen:

$ cargo run > output.txt

>-merkki kertoo shellille, että standardituloste tulee ohjata tiedostoon output.txt. Koska emme nähneet virheilmoitusta terminaalissa, se on todennäköisesti päätynyt tiedostoon. Tarkastetaan tiedoston sisältö:

Problem parsing arguments: not enough arguments

Kyllä, virheilmoitus on kirjoitettu standarditulosteeseen. On hyödyllisempää, että virheilmoitukset tulostetaan standardivirtaan, jotta ne eivät sekoitu ohjelman normaaliin tulosteeseen. Korjaamme tämän seuraavaksi.

Virheiden tulostaminen standardivirtaan eprintln!-makrolla

Koska olemme aiemmin refaktoroineet ohjelmamme, kaikki virheilmoituksia tulostava koodi on yhdessä paikassa, main-funktiossa. Rustin standardikirjasto tarjoaa eprintln!-makron, joka tulostaa standardivirtaan. Vaihdamme nyt println!-kutsut käyttämään sitä.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-24/src/main.rs:here}}
Listing 12-24: Virheilmoitusten tulostaminen standardivirtaan käyttäen eprintln!

Kokeillaan nyt ohjelman suorittamista samalla tavalla kuin aiemmin, ilman argumentteja, ohjaten standardituloste tiedostoon:

$ cargo run > output.txt
Problem parsing arguments: not enough arguments

Nyt virheilmoitus näkyy terminaalissa ja output.txt-tiedosto on tyhjä, mikä on komentoriviohjelmilta odotettu käytös.

Kokeillaan ohjelmaa uudelleen onnistuneella ajolla, mutta ohjaamalla standardituloste tiedostoon:

$ cargo run -- to poem.txt > output.txt

Tässä tapauksessa mitään ei tulosteta terminaaliin, ja tulokset löytyvät output.txt-tiedostosta:

Are you nobody, too?
How dreary to be somebody!

Tämä osoittaa, että käytämme nyt standarditulostetta onnistuneelle tulokselle ja standardivirtaa virheille, mikä on oikein.

Yhteenveto

Tässä luvussa kertasimme monia aiemmin oppimiamme konsepteja ja opimme, miten yleisiä I/O-toimintoja toteutetaan Rustissa. Käyttämällä komentoriviargumentteja, tiedostoja, ympäristömuuttujia ja eprintln!-makroa virheiden tulostamiseen, olemme nyt valmiita kirjoittamaan monipuolisia komentoriviohjelmia.

Aiemmista luvuista opitut periaatteet – koodin organisointi, tietorakenteiden tehokas käyttö, virheiden käsittely ja testaus – yhdistettynä tässä luvussa opittuihin ominaisuuksiin antavat hyvän pohjan jatkaa Rustin käyttöä tehokkaasti.

Seuraavaksi tutustumme joihinkin Rustin funktionaaliseen ohjelmointiin liittyviin ominaisuuksiin: sulkeisiin (closures) ja iteraattoreihin.

Funktionaalisen ohjelmoinnin ominaisuudet: Iteraattorit ja sulkeiset

Rustin suunnittelu on saanut vaikutteita monista eri kielistä ja ohjelmointitekniikoista. Yksi merkittävä vaikutus on tullut funktionaalisesta ohjelmoinnista.

Funktionaalisessa ohjelmointityylissä käytetään usein funktioita arvoina siten, että:

  • Funktiot voidaan antaa argumentteina muille funktioille.
  • Funktiot voivat palauttaa muita funktioita.
  • Funktiot voidaan tallentaa muuttujiin ja suorittaa myöhemmin.

Tässä luvussa emme keskity teoreettisiin määritelmiin siitä, mitä funktionaalinen ohjelmointi on tai ei ole, vaan tarkastelemme Rustin ominaisuuksia, jotka muistuttavat funktionaalisten kielten ominaisuuksia.

Erityisesti käsittelemme:

  • Sulkeisia (closures), jotka ovat muuttujissa säilytettäviä funktiomaisia rakenteita.
  • Iteraattoreita (iterators), joiden avulla voidaan käsitellä peräkkäisiä elementtejä tehokkaasti.
  • Sulkeisten ja iteraattoreiden käyttöä luvun 12 I/O-projektin parantamisessa.
  • Sulkeisten ja iteraattoreiden suorituskykyä (Vihje: ne ovat nopeampia kuin saatat kuvitella!).

Olemme jo käsitelleet joitakin Rustin funktionaalisia ominaisuuksia, kuten mallintamiseen perustuvaa ohjelmointia (pattern matching) ja enum-tyyppejä. Sulkeisten ja iteraattoreiden hallinta on tärkeää idiomaattisen ja suorituskykyisen Rust-koodin kirjoittamisessa, joten omistamme koko luvun näiden aiheiden syventämiseen.

Sulkeiset

Sulkeiset

Rustin sulkeiset ovat anonyymejä funktioita, jotka voidaan tallentaa muuttujaan tai välittää argumenttina toisille funktioille. Sulkeisen voi määrittää yhdessä paikassa ja kutsua sitä myöhemmin toisessa yhteydessä sen arvioimiseksi. Toisin kuin funktiot, sulkeiset voivat siepata arvoja siitä laajuudesta, jossa ne on määritelty. Näytämme, miten nämä sulkeisten ominaisuudet mahdollistavat koodin uudelleenkäytön ja käyttäytymisen mukauttamisen.

Ympäristön sieppaaminen

Tarkastelemme ensin, miten voimme käyttää sulkeisia sieppaamaan arvoja määrittelynsä ympäristöstä myöhempää käyttöä varten. Tässä on skenaario: aika ajoin T-paitayrityksemme antaa eksklusiivisen, rajoitetun erän paidan postituslistallamme olevalle henkilölle kampanjana. Postituslistan jäsenet voivat halutessaan lisätä suosikkivärinsä profiiliinsa. Jos ilmaispaidan voittajalla on suosikkiväri asetettuna, hän saa sen värisen paidan. Jos henkilö ei ole määrittänyt suosikkiväriä, hän saa sen värin, jota yrityksellä on tällä hetkellä eniten varastossa.

Tämän toteuttamiseen on monia tapoja. Tässä esimerkissä käytämme ShirtColor-enumia, jolla on variantit Red ja Blue (rajoitamme värien määrän yksinkertaisuuden vuoksi). Yrityksen varastoa edustaa Inventory-rakenne, jossa on shirts-kenttä, joka sisältää Vec<ShirtColor>-vektorin varastossa olevista paidan väreistä. Inventory-rakenteelle määritelty giveaway-metodi saa ilmaispaidan voittajan valinnaisen paidan väripreferenssin ja palauttaa värin, jonka henkilö saa. Tämä asetelma on esitetty listauksessa 13-1.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-01/src/main.rs}}
Listing 13-1: T-paitayrityksen kampanjatilanne

main-funktiossa määritellyssä store-muuttujassa on kaksi sinistä ja yksi punainen paita jäljellä jaettavaksi tässä rajoitetun erän kampanjassa. Kutsumme giveaway-metodia käyttäjälle, joka haluaa punaisen paidan, ja käyttäjälle, jolla ei ole mitään preferenssiä.

Tämän koodin voisi toteuttaa monin tavoin, ja tässä keskitymme sulkeisiin, joten olemme pysytelleet käsitteissä, jotka olet jo oppinut, lukuun ottamatta giveaway-metodin runkoa, joka käyttää sulkeista. giveaway-metodissa saamme käyttäjän preferenssin parametrina tyypiltä Option<ShirtColor> ja kutsumme unwrap_or_else-metodia user_preference-arvolla. unwrap_or_else-metodi tyypille Option<T> on määritelty standardikirjastossa. Se ottaa yhden argumentin: sulkeisen ilman argumentteja, joka palauttaa arvon T (saman tyypin, joka on tallennettuna Option<T>-tyypin Some-varianttiin; tässä tapauksessa ShirtColor). Jos Option<T> on Some-variantti, unwrap_or_else palauttaa arvon Some-variantin sisältä. Jos Option<T> on None-variantti, unwrap_or_else kutsuu sulkeista ja palauttaa sulkeisen palauttaman arvon.

Määrittelemme sulkeislausekkeen || self.most_stocked() argumentiksi unwrap_or_else-metodille. Tämä on sulkeinen, joka ei itse ota parametreja (jos sulkeisella olisi parametreja, ne näkyisivät kahden pystyviivan välissä). Sulkeisen runko kutsuu self.most_stocked()-metodia. Määrittelemme sulkeisen tässä, ja unwrap_or_else-metodin toteutus arvioi sulkeisen myöhemmin, jos tulos tarvitaan.

Tämän koodin suorittaminen tulostaa:

{{#include ../listings/ch13-functional-features/listing-13-01/output.txt}}

Yksi mielenkiintoinen näkökohta on, että olemme välittäneet sulkeisen, joka kutsuu self.most_stocked()-metodia nykyisellä Inventory-instanssilla. Standardikirjaston ei tarvinnut tietää mitään määrittelemistämme Inventory- tai ShirtColor-tyypeistä tai logiikasta, jota haluamme käyttää tässä skenaariossa. Sulkeinen sieppaa muuttumattoman viittauksen self-Inventory-instanssiin ja välittää sen määrittelemämme koodin kanssa unwrap_or_else-metodille. Funktiot puolestaan eivät pysty sieppaamaan ympäristöään tällä tavalla.

Sulkeisten tyyppipäätelmä ja annotointi

Funktioiden ja sulkeisten välillä on muitakin eroja. Sulkeiset eivät yleensä vaadi parametrien tai palautusarvon tyyppien annotointia kuten fn-funktiot. Funktioissa tyyppiannotaatiot ovat pakollisia, koska tyypit ovat osa käyttäjille paljastettua eksplisiittistä rajapintaa. Tämän rajapinnan jäykkä määrittely on tärkeää varmistaakseen, että kaikki ovat samaa mieltä siitä, minkä tyyppisiä arvoja funktio käyttää ja palauttaa. Sulkeisia ei puolestaan käytetä tällaisessa paljastetussa rajapinnassa: ne tallennetaan muuttujiin ja käytetään nimeämättä niitä ja paljastamatta niitä kirjastomme käyttäjille.

Sulkeiset ovat tyypillisesti lyhyitä ja merkityksellisiä vain kapeassa kontekstissa eivätkä missä tahansa satunnaisessa skenaariossa. Näissä rajallisissa konteksteissa kääntäjä pystyy päättelemään parametrien ja palautustyypin tyypit, samoin kuin se pystyy päättelemään useimpien muuttujien tyypit (on harvinaisia tapauksia, joissa kääntäjä tarvitsee myös sulkeisen tyyppiannotaatioita).

Kuten muuttujien kanssa, voimme lisätä tyyppiannotaatioita, jos haluamme lisätä eksplisiittisyyttä ja selkeyttä kustannuksella siitä, että olemme sanallisempia kuin on ehdottoman välttämätöntä. Sulkeisen tyyppien annotointi näyttäisi listauksessa 13-2 esitetyltä määrittelyltä. Tässä esimerkissä määrittelemme sulkeisen ja tallennamme sen muuttujaan sen sijaan, että määrittäisimme sulkeisen siinä kohdassa, jossa välitämme sen argumenttina, kuten teimme listauksessa 13-1.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-02/src/main.rs:here}}
}
Listing 13-2: Sulkeisen parametrin ja palautusarvon tyyppien valinnaisten tyyppiannotaatioiden lisääminen

Tyyppiannotaatioiden lisäämisen jälkeen sulkeisten syntaksi näyttää enemmän funktioiden syntaksilta. Tässä määrittelemme funktion, joka lisää 1 parametrilleen, ja sulkeisen, jolla on sama käyttäytyminen, vertailua varten. Olemme lisänneet välilyöntejä kohdistaaksemme asiaankuuluvat osat. Tämä havainnollistaa, miten sulkeisten syntaksi on samankaltainen kuin funktioiden syntaksi, paitsi pystyviivojen käyttö ja valinnaisen syntaksin määrä:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Ensimmäinen rivi näyttää funktion määrittelyn, ja toinen rivi näyttää täysin annotoidun sulkeisen määrittelyn. Kolmannella rivillä poistamme tyyppiannotaatiot sulkeisen määrittelystä. Neljännellä rivillä poistamme aaltosulkeet, jotka ovat valinnaisia, koska sulkeisen rungossa on vain yksi lauseke. Nämä ovat kaikki kelvollisia määrittelyjä, jotka tuottavat saman käyttäytymisen, kun niitä kutsutaan. add_one_v3- ja add_one_v4-rivit vaativat sulkeisten arviointia, jotta ne voivat kääntyä, koska tyypit päätellään niiden käytöstä. Tämä on samankaltaista kuin let v = Vec::new();, joka vaatii joko tyyppiannotaatioita tai jonkin tyypin arvoja lisättäväksi Vec-kokoelmaan, jotta Rust pystyy päättelemään tyypin.

Sulkeisten määrittelyissä kääntäjä päättelee yhden konkreettisen tyypin kullekin niiden parametrille ja palautusarvolle. Esimerkiksi listaus 13-3 näyttää lyhyen sulkeisen määrittelyn, joka vain palauttaa parametrina saamansa arvon. Tämä sulkeinen ei ole kovin hyödyllinen muuten kuin tämän esimerkin tarkoituksia varten. Huomaa, ettemme ole lisänneet määrittelyyn mitään tyyppiannotaatioita. Koska tyyppiannotaatioita ei ole, voimme kutsua sulkeista millä tahansa tyypillä, minkä olemme tehneet tässä ensimmäisellä kerralla String-tyypillä. Jos yritämme sitten kutsua example_closure-sulkeista kokonaisluvulla, saamme virheen.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-03/src/main.rs:here}}
Listing 13-3: Yritys kutsua sulkeista, jonka tyypit on päätelty, kahdella eri tyypillä

Kääntäjä antaa meille tämän virheen:

{{#include ../listings/ch13-functional-features/listing-13-03/output.txt}}

Ensimmäisellä kerralla, kun kutsumme example_closure-sulkeista String-arvolla, kääntäjä päättelee x:n tyypiksi ja sulkeisen palautustyypiksi String. Nämä tyypit lukitaan sitten example_closure-sulkeiseen, ja saamme tyyppivirheen, kun yritämme seuraavaksi käyttää eri tyyppiä saman sulkeisen kanssa.

Viittausten sieppaaminen tai omistajuuden siirtäminen

Sulkeiset voivat siepata arvoja ympäristöstään kolmella tavalla, jotka vastaavat suoraan kolmea tapaa, joilla funktio voi ottaa parametrin: lainata muuttumattomasti, lainata muuttuvasti ja ottaa omistajuuden. Sulkeinen päättää, kumpaa näistä käyttää, sen perusteella, mitä funktion runko tekee siepatuille arvoille.

Listauksessa 13-4 määrittelemme sulkeisen, joka sieppaa muuttumattoman viittauksen list-nimiseen vektoriin, koska se tarvitsee vain muuttumattoman viittauksen arvon tulostamiseen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-04/src/main.rs}}
}
Listing 13-4: Muuttumattoman viittauksen sieppaavan sulkeisen määrittely ja kutsuminen

Tämä esimerkki havainnollistaa myös, että muuttuja voi sitoutua sulkeisen määrittelyyn, ja voimme myöhemmin kutsua sulkeista käyttämällä muuttujan nimeä ja sulkeita ikään kuin muuttujan nimi olisi funktion nimi.

Koska voimme olla samanaikaisesti useita muuttumattomia viittauksia list-muuttujaan, list on edelleen käytettävissä koodista ennen sulkeisen määrittelyä, sulkeisen määrittelyn jälkeen mutta ennen sulkeisen kutsumista ja sulkeisen kutsumisen jälkeen. Tämä koodi kääntyy, suorittuu ja tulostaa:

{{#include ../listings/ch13-functional-features/listing-13-04/output.txt}}

Seuraavaksi listauksessa 13-5 muutamme sulkeisen runkoa niin, että se lisää elementin list-vektoriin. Sulkeinen sieppaa nyt muuttuvan viittauksen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-05/src/main.rs}}
}
Listing 13-5: Muuttuvan viittauksen sieppaavan sulkeisen määrittely ja kutsuminen

Tämä koodi kääntyy, suorittuu ja tulostaa:

{{#include ../listings/ch13-functional-features/listing-13-05/output.txt}}

Huomaa, ettei println!-kutsua ole enää borrows_mutably-sulkeisen määrittelyn ja kutsumisen välissä: kun borrows_mutably määritellään, se sieppaa muuttuvan viittauksen list-muuttujaan. Emme käytä sulkeista uudelleen sen kutsumisen jälkeen, joten muuttuva laina päättyy. Sulkeisen määrittelyn ja kutsumisen välissä muuttumatonta lainaa tulostusta varten ei sallita, koska muita lainoja ei sallita, kun on muuttuva laina. Kokeile lisätä println!-kutsu sinne ja katso, millaisen virheilmoituksen saat!

Jos haluat pakottaa sulkeisen ottamaan omistajuuden arvoista, joita se käyttää ympäristössään, vaikka sulkeisen runko ei ehdottomasti tarvitsisikaan omistajuutta, voit käyttää move-avainsanaa parametrilistan edessä.

Tämä tekniikka on enimmäkseen hyödyllinen, kun välitetään sulkeinen uudelle säikeelle siirtämään data niin, että uusi säie omistaa sen. Käsittelemme säikeitä ja syitä niiden käyttöön yksityiskohtaisesti luvussa 16 puhuessamme rinnakkaisuudesta, mutta tutkitaan nyt lyhyesti uuden säikeen luomista sulkeisella, joka tarvitsee move-avainsanan. Listaus 13-6 näyttää listauksen 13-4 muutetun version, joka tulostaa vektorin uudessa säikeessä pääsäikeen sijaan:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-06/src/main.rs}}
}
Listing 13-6: move-avainsanan käyttö pakottamaan säikeen sulkeinen ottamaan omistajuuden list-muuttujasta

Luomme uuden säikeen antamalla säikeelle sulkeisen argumenttina suoritettavaksi. Sulkeisen runko tulostaa listan. Listauksessa 13-4 sulkeinen sieppasi list-muuttujan vain muuttumattomalla viittauksella, koska se on vähimmän määrä pääsyä list-muuttujaan sen tulostamiseen. Tässä esimerkissä, vaikka sulkeisen runko tarvitsee edelleen vain muuttumattoman viittauksen, meidän täytyy määrittää, että list siirretään sulkeiseen asettamalla move-avainsana sulkeisen määrittelyn alkuun. Jos pääsäie suorittaisi lisää operaatioita ennen kuin kutsuu join-metodia uudella säikeellä, uusi säie saattaa valmistua ennen kuin pääsäie valmistuu, tai pääsäie saattaa valmistua ensin. Jos pääsäie säilyttäisi omistajuuden list-muuttujaan mutta päättyisi ennen uutta säiettä ja pudottaisi list-muuttujan, säikeen muuttumaton viittaus olisi virheellinen. Siksi kääntäjä vaatii, että list siirretään uudelle säikeelle annettuun sulkeiseen, jotta viittaus olisi kelvollinen. Kokeile poistaa move-avainsana tai käyttää list-muuttujaa pääsäikeessä sulkeisen määrittelyn jälkeen ja katso, mitä kääntäjävirheitä saat!

Siepatun arvon siirtäminen sulkeisesta ulos

Kun sulkeinen on siepannut viittauksen tai ottanut omistajuuden arvosta sulkeisen määrittelypaikan ympäristöstä (vaikuttaen siihen, mitä, jos mitään, siirretään sulkeiseen sisään), sulkeisen rungon koodi määrittää, mitä viittauksille tai arvoille tapahtuu, kun sulkeinen arvioidaan myöhemmin (vaikuttaen siihen, mitä, jos mitään, siirretään sulkeisesta ulos).

Sulkeisen runko voi tehdä mitä tahansa seuraavista: siirtää siepatun arvon sulkeisesta ulos, mutatoida siepattua arvoa, ei siirtää eikä mutatoida arvoa, tai olla alun perin sieppaamatta mitään ympäristöstä.

Tapa, jolla sulkeinen sieppaa ja käsittelee arvoja ympäristöstä, vaikuttaa siihen, mitä traitteja sulkeinen toteuttaa, ja traittien avulla funktiot ja rakenteet voivat määrittää, millaisia sulkeisia ne voivat käyttää. Sulkeiset toteuttavat automaattisesti yhden, kaksi tai kaikki kolme näistä Fn-traiteista additiivisesti sen perusteella, miten sulkeisen runko käsittelee arvoja:

  • FnOnce koskee sulkeisia, joita voidaan kutsua kerran. Kaikki sulkeiset toteuttavat vähintään tämän traitin, koska kaikkia sulkeisia voidaan kutsua. Sulkeinen, joka siirtää siepatut arvot rungostaan ulos, toteuttaa vain FnOnce-traitin eikä mitään muista Fn-traiteista, koska sitä voidaan kutsua vain kerran.
  • FnMut koskee sulkeisia, jotka eivät siirrä siepattuja arvoja rungostaan ulos, mutta jotka saattavat mutatoida siepattuja arvoja. Näitä sulkeisia voidaan kutsua useammin kuin kerran.
  • Fn koskee sulkeisia, jotka eivät siirrä siepattuja arvoja rungostaan ulos eivätkä mutatoi siepattuja arvoja, sekä sulkeisia, jotka eivät sieppaa mitään ympäristöstään. Näitä sulkeisia voidaan kutsua useammin kuin kerran mutatoimatta ympäristöään, mikä on tärkeää tapauksissa, kuten sulkeisen kutsumisessa useita kertoja rinnakkain.

Katsotaan unwrap_or_else-metodin määrittelyä tyypille Option<T>, jota käytimme listauksessa 13-1:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

Muista, että T on geneerinen tyyppi, joka edustaa arvon tyyppiä Option-tyypin Some-variantissa. Tyyppi T on myös unwrap_or_else-funktion palautustyyppi: koodi, joka kutsuu unwrap_or_else-metodia Option<String>-arvolla, esimerkiksi, saa String-arvon.

Seuraavaksi huomaa, että unwrap_or_else-funktiolla on lisägeneerinen tyyppiparametri F. F-tyyppi on parametrin f tyyppi, joka on sulkeinen, jonka annamme kutsuessamme unwrap_or_else-metodia.

Geneeriselle tyypille F määritelty trait-raja on FnOnce() -> T, mikä tarkoittaa, että F:n täytyy voida kutsua kerran, olla ilman argumentteja ja palauttaa T. FnOnce-traitin käyttö trait-rajassa ilmaisee rajoitteen, että unwrap_or_else ei kutsu f:ää useammin kuin kerran. unwrap_or_else-metodin rungossa näemme, että jos Option on Some, f:ää ei kutsuta. Jos Option on None, f:ää kutsutaan kerran. Koska kaikki sulkeiset toteuttavat FnOnce-traitin, unwrap_or_else hyväksyy kaikki kolme sulkeistyyppiä ja on niin joustava kuin mahdollista.

Huom: Jos haluamamme ei vaadi arvon sieppaamista ympäristöstä, voimme käyttää funktion nimeä sulkeisen sijaan siellä, missä tarvitsemme jotain, joka toteuttaa jonkin Fn-traiteista. Voimme esimerkiksi kutsua unwrap_or_else(Vec::new)-metodia Option<Vec<T>>-arvolla saadaksemme uuden tyhjän vektorin, jos arvo on None. Kääntäjä toteuttaa automaattisesti sen Fn-traiteista, joka soveltuu funktion määrittelyyn.

Katsotaan nyt standardikirjaston sort_by_key-metodin määrittelyä viipaleille nähdäksemme, miten se eroaa unwrap_or_else-metodista ja miksi sort_by_key käyttää FnMut-traitia FnOnce-traitin sijaan trait-rajana. Sulkeinen saa yhden argumentin viittauksena tarkasteltavan viipaleen nykyiseen alkioon ja palauttaa järjestettävän tyypin K arvon. Tämä funktio on hyödyllinen, kun haluat järjestää viipaleen jonkin kunkin alkion ominaisuuden mukaan. Listauksessa 13-7 meillä on lista Rectangle-instansseja, ja käytämme sort_by_key-metodia järjestääksemme ne width-ominaisuuden mukaan alhaalta ylös.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-07/src/main.rs}}
}
Listing 13-7: sort_by_key-metodin käyttö suorakulmioiden järjestämiseen leveyden mukaan

Tämä koodi tulostaa:

{{#include ../listings/ch13-functional-features/listing-13-07/output.txt}}

Syy siihen, että sort_by_key on määritelty ottamaan FnMut-sulkeinen, on se, että se kutsuu sulkeista useita kertoja: kerran kullekin viipaleen alkiolle. Sulkeinen |r| r.width ei sieppaa, mutatoi tai siirrä mitään ulos ympäristöstään, joten se täyttää trait-rajavaatimukset.

Sitä vastoin listaus 13-8 näyttää esimerkin sulkeisesta, joka toteuttaa vain FnOnce-traitin, koska se siirtää arvon ympäristöstään ulos. Kääntäjä ei anna meidän käyttää tätä sulkeista sort_by_key-metodin kanssa.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-08/src/main.rs}}
Listing 13-8: Yritys käyttää FnOnce-sulkeista sort_by_key-metodin kanssa

Tämä on keinotekoinen, mutkikas tapa (joka ei toimi) yrittää laskea, kuinka monta kertaa sort_by_key kutsuu sulkeista järjestäessään list-muuttujaa. Tämä koodi yrittää tehdä tämän laskennan työntämällä value-muuttujan — String-arvon sulkeisen ympäristöstä — sort_operations-vektoriin. Sulkeinen sieppaa value-muuttujan ja siirtää sen sitten sulkeisesta ulos siirtämällä value-muuttujan omistajuuden sort_operations-vektoriin. Tätä sulkeista voidaan kutsua kerran; yritys kutsua sitä toisen kerran ei toimisi, koska value ei olisi enää ympäristössä työnnettäväksi sort_operations-vektoriin uudelleen! Siksi tämä sulkeinen toteuttaa vain FnOnce-traitin. Kun yritämme kääntää tämän koodin, saamme tämän virheen, että value-muuttujaa ei voi siirtää sulkeisesta ulos, koska sulkeisen täytyy toteuttaa FnMut:

{{#include ../listings/ch13-functional-features/listing-13-08/output.txt}}

Virhe osoittaa sulkeisen rungon rivin, joka siirtää value-muuttujan ympäristöstä ulos. Korjataksemme tämän meidän täytyy muuttaa sulkeisen runkoa niin, ettei se siirrä arvoja ympäristöstä ulos. Laskeaksemme, kuinka monta kertaa sulkeista kutsutaan, laskurin pitäminen ympäristössä ja sen arvon kasvattaminen sulkeisen rungossa on suoraviivaisempi tapa. Listauksen 13-9 sulkeinen toimii sort_by_key-metodin kanssa, koska se sieppaa vain muuttuvan viittauksen num_sort_operations-laskuriin ja voidaan siksi kutsua useammin kuin kerran.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-09/src/main.rs}}
}
Listing 13-9: FnMut-sulkeisen käyttö sort_by_key-metodin kanssa on sallittua

Fn-traitit ovat tärkeitä, kun määritellään tai käytetään funktioita tai tyyppejä, jotka käyttävät sulkeisia. Seuraavassa osiossa käsittelemme iteraattoreita. Monet iteraattorimetodit ottavat sulkeisargumentteja, joten pidä nämä sulkeisten yksityiskohdat mielessä jatkaessamme!

Kohteiden sarjojen käsittely iteraattoreilla

Kohteiden sarjojen käsittely iteraattoreilla

Iteraattorimalli mahdollistaa tehtävien suorittamisen sarjalle kohteita vuorotellen. Iteraattori huolehtii logiikasta, jolla käydään läpi jokainen kohde ja määritetään, milloin sarja on päättynyt. Käyttäessäsi iteraattoreita sinun ei tarvitse toteuttaa tätä logiikkaa uudelleen itse.

Rustissa iteraattorit ovat laiskoja, eli niillä ei ole vaikutusta ennen kuin kutsut metodeja, jotka kuluttavat iteraattorin sen käyttämiseksi loppuun. Esimerkiksi listauksen 13-10 koodi luo iteraattorin vektorin v1 alkioille kutsumalla Vec<T>-tyypille määriteltyä iter-metodia. Tämä koodi ei itsessään tee mitään hyödyllistä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-10/src/main.rs:here}}
}
Listing 13-10: Iteraattorin luominen

Iteraattori tallennetaan muuttujaan v1_iter. Kun iteraattori on luotu, voimme käyttää sitä monin eri tavoin. Listauksessa 3-5 iteroimme taulukkoa for-silmukalla suorittaaksemme koodia jokaiselle sen alkioille. Taustalla tämä loi ja kulutti implisiittisesti iteraattorin, mutta ohitimme siihen asti tarkalleen, miten se toimii.

Listauksen 13-11 esimerkissä erotamme iteraattorin luonnin iteraattorin käytöstä for-silmukassa. Kun for-silmukkaa kutsutaan käyttäen iteraattoria v1_iter-muuttujassa, jokaista iteraattorin alkiota käytetään yhdessä silmukan kierroksessa, jolloin jokainen arvo tulostetaan.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-11/src/main.rs:here}}
}
Listing 13-11: Iteraattorin käyttö for-silmukassa

Kielissä, joiden standardikirjasto ei tarjoa iteraattoreita, kirjoittaisit todennäköisesti saman toiminnallisuuden aloittamalla muuttujan arvosta 0, käyttämällä tuota muuttujaa indeksinä vektoriin arvon hakemiseksi ja kasvattamalla muuttujan arvoa silmukassa, kunnes se saavuttaa vektorin alkiojen kokonaismäärän.

Iteraattorit hoitavat koko tämän logiikan puolestasi, vähentäen toistuvaa koodia, jonka voisit potentiaalisesti kirjoittaa väärin. Iteraattorit antavat sinulle enemmän joustavuutta käyttää samaa logiikkaa monenlaisissa sarjoissa, eivätkä ne rajoitu vain tietorakenteisiin, joihin pääsee käsiksi indeksin avulla, kuten vektoreihin. Tarkastellaan, miten iteraattorit tekevät sen.

Iterator-trait ja next-metodi

Kaikki iteraattorit toteuttavat standardikirjastossa määritellyn Iterator-nimisen traitin. Traitin määritelmä näyttää tältä:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}
}

Huomaa, että tämä määritelmä käyttää uutta syntaksia: type Item ja Self::Item, jotka määrittelevät tälle traitille liittyvän tyypin. Käsittelemme liittyviä tyyppejä perusteellisesti luvussa 20. Toistaiseksi riittää tietää, että tämä koodi sanoo, että Iterator-traitin toteuttaminen edellyttää myös Item-tyypin määrittelyä, ja tätä Item-tyyppiä käytetään next-metodin palautustyypissä. Toisin sanoen Item-tyyppi on tyyppi, jonka iteraattori palauttaa.

Iterator-trait edellyttää toteuttajilta vain yhden metodin määrittelyä: next-metodin, joka palauttaa yhdellä kerralla yhden iteraattorin alkion käärittynä Some-arvoon ja kun iterointi on päättynyt, palauttaa None-arvon.

Voimme kutsua next-metodia iteraattoreilla suoraan; listaus 13-12 havainnollistaa, mitä arvoja palautetaan toistuvilla next-kutsuilla iteraattorille, joka on luotu vektorista.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-12/src/lib.rs:here}}
Listing 13-12: next-metodin kutsuminen iteraattorilla

Huomaa, että meidän täytyi tehdä v1_iter muuttuvaksi: iteraattorin next-metodin kutsuminen muuttaa sisäistä tilaa, jota iteraattori käyttää seuratakseen sijaintiaan sarjassa. Toisin sanoen tämä koodi kuluttaa eli käyttää iteraattorin loppuun. Jokainen next-kutsu kuluttaa yhden alkion iteraattorista. Meidän ei tarvinnut tehdä v1_iter-muuttujasta muuttuvaa käyttäessämme for-silmukkaa, koska silmukka otti omistajuuden v1_iter-muuttujasta ja teki sen muuttuvaksi taustalla.

Huomaa myös, että next-kutsuista saamamme arvot ovat muuttumattomia viittauksia vektorin arvoihin. iter-metodi tuottaa iteraattorin muuttumattomien viittausten yli. Jos haluamme luoda iteraattorin, joka ottaa omistajuuden v1-muuttujasta ja palauttaa omistettuja arvoja, voimme kutsua into_iter-metodia iter-metodin sijaan. Vastaavasti, jos haluamme iteroida muuttuvien viittausten yli, voimme kutsua iter_mut-metodia iter-metodin sijaan.

Iteraattorin kuluttavat metodit

Iterator-traitilla on useita eri metodeja, joille standardikirjasto tarjoaa oletustoteutukset; näistä metodeista voi lukea standardikirjaston API-dokumentaatiosta Iterator-traitin kohdalta. Jotkin näistä metodeista kutsuvat määritelmässään next-metodia, minkä vuoksi Iterator-traitin toteuttamisessa on pakollista toteuttaa next-metodi.

Metodeja, jotka kutsuvat next-metodia, kutsutaan kuluttaviksi sovittimiksi, koska niiden kutsuminen käyttää iteraattorin loppuun. Yksi esimerkki on sum-metodi, joka ottaa omistajuuden iteraattorista ja iteroi alkioiden läpi kutsumalla toistuvasti next-metodia ja näin kuluttaen iteraattorin. Iteroidessaan se lisää jokaisen alkion käynnissä olevaan summaan ja palauttaa summan, kun iterointi on valmis. Listauksessa 13-13 on testi, joka havainnollistaa sum-metodin käyttöä.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-13/src/lib.rs:here}}
Listing 13-13: sum-metodin kutsuminen iteraattorin kaikkien alkioiden summan saamiseksi

Emme saa käyttää v1_iter-muuttujaa sum-kutsun jälkeen, koska sum ottaa omistajuuden iteraattorista, jolle sitä kutsutaan.

Muita iteraattoreja tuottavat metodit

Iteraattorisovittimet ovat Iterator-traitille määriteltyjä metodeja, jotka eivät kuluta iteraattoria. Sen sijaan ne tuottavat eri iteraattoreja muuttamalla jotakin alkuperäisen iteraattorin ominaisuutta.

Listaus 13-14 näyttää esimerkin iteraattorisovitinmetodin map kutsumisesta, joka ottaa sulkeisen kutsumista varten jokaiselle alkioille, kun alkioiden läpi iteroidaan. map-metodi palauttaa uuden iteraattorin, joka tuottaa muokatut alkiot. Tässä sulkeinen luo uuden iteraattorin, jossa vektorin jokainen alkio kasvatetaan yhdellä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-14/src/main.rs:here}}
}
Listing 13-14: Iteraattorisovittimen map kutsuminen uuden iteraattorin luomiseksi

Tämä koodi tuottaa kuitenkin varoituksen:

{{#include ../listings/ch13-functional-features/listing-13-14/output.txt}}

Listauksen 13-14 koodi ei tee mitään; määrittämäämme sulkeista ei koskaan kutsuta. Varoitus muistuttaa miksi: iteraattorisovittimet ovat laiskoja, ja meidän täytyy kuluttaa iteraattori tässä.

Varoituksen korjaamiseksi ja iteraattorin kuluttamiseksi käytämme collect-metodia, jota käytimme env::args-funktion kanssa listauksessa 12-1. Tämä metodi kuluttaa iteraattorin ja kerää tuloksena syntyvät arvot kokoelmatietotyypiksi.

Listauksessa 13-15 keräämme map-kutsusta palautetun iteraattorin iteroinnin tulokset vektoriksi. Tämä vektori sisältää lopulta jokaisen alkuperäisen vektorin alkion kasvatettuna yhdellä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-15/src/main.rs:here}}
}
Listing 13-15: map-metodin kutsuminen uuden iteraattorin luomiseksi ja sitten collect-metodin kutsuminen uuden iteraattorin kuluttamiseksi ja vektorin luomiseksi

Koska map ottaa sulkeisen, voimme määrittää minkä tahansa operaation, jonka haluamme suorittaa jokaiselle alkioille. Tämä on erinomainen esimerkki siitä, miten sulkeiset antavat sinun mukauttaa käyttäytymistä samalla kun hyödynnät uudelleen Iterator-traitin tarjoamaa iterointikäyttäytymistä.

Voit ketjuttaa useita iteraattorisovitinmetodien kutsuja suorittaaksesi monimutkaisia toimintoja luettavalla tavalla. Koska kaikki iteraattorit ovat kuitenkin laiskoja, sinun täytyy kutsua yhtä kuluttavista sovitinmetodeista saadaksesi tuloksia iteraattorisovitinmetodien kutsuista.

Ympäristönsä sieppaavat sulkeiset

Monet iteraattorisovittimet ottavat sulkeisia argumentteina, ja usein iteraattorisovittimille määrittämämme sulkeiset ovat sulkeisia, jotka sieppaavat ympäristönsä.

Tätä esimerkkiä varten käytämme filter-metodia, joka ottaa sulkeisen. Sulkeinen saa alkion iteraattorista ja palauttaa bool-arvon. Jos sulkeinen palauttaa true, arvo sisällytetään filter-metodin tuottamaan iteraattoriin. Jos sulkeinen palauttaa false, arvoa ei sisällytetä.

Listauksessa 13-16 käytämme filter-metodia sulkeisella, joka sieppaa shoe_size-muuttujan ympäristöstään iteroidakseen Shoe-rakenteen instanssien kokoelman yli. Se palauttaa vain kengät, joiden koko on määritetty.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-16/src/lib.rs}}
Listing 13-16: filter-metodin käyttö shoe_size-muuttujaa sieppaavan sulkeisen kanssa

shoes_in_size-funktio ottaa omistajuuden kenkävektorista ja kenkäkoosta parametreina. Se palauttaa vektorin, joka sisältää vain määritetyn kokoiset kengät.

shoes_in_size-funktion rungossa kutsumme into_iter-metodia luodaksemme iteraattorin, joka ottaa omistajuuden vektorista. Sen jälkeen kutsumme filter-metodia mukauttaaksemme iteraattorin uudeksi iteraattoriksi, joka sisältää vain ne alkiot, joille sulkeinen palauttaa true-arvon.

Sulkeinen sieppaa shoe_size-parametrin ympäristöstä ja vertaa arvoa kunkin kengän kokoon, säilyttäen vain määritetyn kokoiset kengät. Lopuksi collect-kutsu kerää mukautetun iteraattorin palauttamat arvot vektoriin, jonka funktio palauttaa.

Testi osoittaa, että kun kutsumme shoes_in_size-funktiota, saamme takaisin vain kengät, joiden koko on sama kuin määrittämämme arvo.

I/O-projektimme parantaminen

I/O-projektimme parantaminen

Tämän uuden iteraattoritietämyksen avulla voimme parantaa luvun 12 I/O-projektia käyttämällä iteraattoreita koodin selkeyttämiseen ja tiivistämiseen. Katsotaan, miten iteraattorit voivat parantaa Config::build- ja search-funktioiden toteutusta.

clone-kutsun poistaminen iteraattorilla

Listauksessa 12-6 lisäsimme koodia, joka otti String-arvojen viipaleen ja loi Config-rakenteen instanssin indeksoimalla viipaleeseen ja kloonaamalla arvot, jotta Config-rakenne voi omistaa ne. Listauksessa 13-17 olemme toistaneet Config::build-funktion toteutuksen sellaisena kuin se oli listauksessa 12-23.

Silloin sanoin, ettei tehokkaiden clone-kutsujen tarvitse huolestuttaa, koska poistamme ne myöhemmin. No, se aika on nyt!

Tarvitsimme clone-kutsun, koska parametrissa args on String-elementtien viipale, mutta build-funktio ei omista args-parametria. Palauttaaksemme Config-instanssin omistajuuden meidän täytyi kloonata arvot query- ja file_path-kentistä, jotta Config-instanssi voi omistaa arvonsa.

Uudella iteraattoritietämyksellämme voimme muuttaa build-funktion ottamaan omistajuuden iteraattorista argumenttina viipaleen lainaamisen sijaan. Käytämme iteraattorin toiminnallisuutta viipaleen pituuden tarkistamiseen ja tiettyihin paikkoihin indeksoimiseen. Tämä selkeyttää, mitä Config::build tekee, koska iteraattori käyttää arvoja.

Kun Config::build ottaa omistajuuden iteraattorista eikä enää käytä lainaavia indeksointioperaatioita, voimme siirtää String-arvot iteraattorista Config-rakenteeseen clone-kutsun ja uuden allokoinnin sijaan.

Palautetun iteraattorin käyttö suoraan

Avaa I/O-projektisi tiedosto src/main.rs, jonka pitäisi näyttää tältä:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-12-24-reproduced/src/main.rs:ch13}}

Muutamme ensin main-funktion alun listauksesta 12-24 listauksen 13-18 koodiin, joka tällä kertaa käyttää iteraattoria. Tämä ei käänny ennen kuin päivitämme myös Config::build-funktion.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-18/src/main.rs:here}}
Listing 13-18: env::args-funktion palautusarvon välittäminen Config::build-funktiolle

env::args-funktio palauttaa iteraattorin! Sen sijaan, että keräisimme iteraattorin arvot vektoriin ja välittäisimme viipaleen Config::build-funktiolle, välitämme nyt suoraan omistajuuden env::args-funktion palauttamaan iteraattoriin Config::build-funktiolle.

Seuraavaksi päivitämme Config::build-funktion määrittelyn. Muutetaan Config::build-funktion allekirjoitus näyttämään listauksen 13-19 mukaiselta. Tämäkään ei vielä käänny, koska meidän täytyy päivittää funktion runko.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-19/src/main.rs:here}}
Listing 13-19: Config::build-funktion allekirjoituksen päivittäminen odottamaan iteraattoria

Standardikirjaston dokumentaatio env::args-funktiolle näyttää, että sen palauttaman iteraattorin tyyppi on std::env::Args, ja tämä tyyppi toteuttaa Iterator-traitin ja palauttaa String-arvoja.

Olemme päivittäneet Config::build-funktion allekirjoituksen niin, että parametrilla args on geneerinen tyyppi trait-rajoilla impl Iterator<Item = String> &[String]-tyypin sijaan. Tämä impl Trait -syntaksin käyttö, josta puhuimme luvun 10 osiossa ”Traitien käyttö parametreina”, tarkoittaa, että args voi olla mikä tahansa tyyppi, joka toteuttaa Iterator-traitin ja palauttaa String-kohteita.

Koska otamme omistajuuden args-parametrista ja muutamme sitä iteroinnin aikana, voimme lisätä mut-avainsanan args-parametrin määrittelyyn tehdäksemme siitä muuttuvan.

Iterator-traitin metodien käyttö

Seuraavaksi korjaamme Config::build-funktion rungon. Koska args toteuttaa Iterator-traitin, tiedämme voivamme kutsua sille next-metodia! Listaus 13-20 päivittää listauksen 12-23 koodin käyttämään next-metodia.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-20/src/main.rs:here}}
Listing 13-20: Config::build-funktion rungon muuttaminen käyttämään iteraattorimetodeja

Muista, että env::args-funktion palautusarvon ensimmäinen arvo on ohjelman nimi. Haluamme jättää sen huomiotta ja siirtyä seuraavaan arvoon, joten kutsumme ensin next-metodia tekemättä mitään palautusarvolla. Sitten kutsumme next-metodia saadaksemme arvon, jonka haluamme Config-rakenteen query-kenttään. Jos next palauttaa Some-variantin, käytämme match-lausetta arvon purkamiseen. Jos se palauttaa None-variantin, argumentteja ei annettu tarpeeksi, ja palaamme aikaisin Err-arvolla. Teemme saman file_path-arvolle.

Koodin selkeyttäminen iteraattorisovittimilla

Voimme hyödyntää iteraattoreita myös I/O-projektimme search-funktiossa, joka on toistettu tässä listauksessa 13-21 sellaisena kuin se oli listauksessa 12-19.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:ch13}}
Listing 13-21: search-funktion toteutus listauksesta 12-19

Voimme kirjoittaa tämän koodin tiiviimmin iteraattorisovittimien avulla. Samalla voimme välttää väliaikaisen muuttuvan results-vektorin. Funktionaalinen ohjelmointityyli pyrkii minimoimaan muuttuvan tilan määrän koodin selkeyttämiseksi. Muuttuvan tilan poistaminen voisi mahdollistaa tulevaisuudessa rinnakkaisen haun, koska emme joutuisi hallitsemaan samanaikaista pääsyä results-vektoriin. Listaus 13-22 näyttää tämän muutoksen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-22/src/lib.rs:here}}
Listing 13-22: Iteraattorisovittimien käyttö search-funktion toteutuksessa

Muista, että search-funktion tarkoitus on palauttaa kaikki contents-merkkijonon rivit, jotka sisältävät query-merkkijonon. Kuten filter-esimerkissä listauksessa 13-16, tämä koodi käyttää filter-sovitinta pitämään vain rivit, joille line.contains(query) palauttaa true. Keräämme sitten vastaavat rivit toiseen vektoriin collect-metodilla. Paljon yksinkertaisempaa! Voit vapaasti tehdä saman muutoksen search_case_insensitive-funktioon käyttämällä iteraattorimetodeja.

Lisäparannuksena voit palauttaa iteraattorin search-funktiosta poistamalla collect-kutsun ja muuttamalla palautustyypiksi impl Iterator<Item = &'a str>, jolloin funktiosta tulee iteraattorisovitin. Huomaa, että sinun täytyy päivittää myös testit! Etsi suuresta tiedostosta minigrep-työkalullasi ennen ja jälkeen muutoksen tehdessäsi ja havainnoi käyttäytymisen eroa. Ennen muutosta ohjelma ei tulosta tuloksia ennen kuin se on kerännyt kaikki tulokset, mutta muutoksen jälkeen tulokset tulostetaan sitä mukaa kun vastaava rivi löytyy, koska run-funktion for-silmukka hyödyntää iteraattorin laiskuutta.

Silmukoiden ja iteraattorien valinta

Seuraava looginen kysymys on, kumpaa tyyliä sinun kannattaa käyttää omassa koodissasi ja miksi: alkuperäinen toteutus listauksessa 13-21 vai iteraattoreita käyttävä versio listauksessa 13-22 (olettaen, että keräämme kaikki tulokset ennen palauttamista iteraattorin sijaan). Useimmat Rust-ohjelmoijat suosivat iteraattorityyliä. Se on aluksi hieman vaikeampi omaksua, mutta kun saat tuntuman eri iteraattorisovittimista ja niiden toiminnasta, iteraattorit voivat olla helpommin ymmärrettäviä. Sen sijaan, että säätäisit silmukan eri osia ja rakentaisit uusia vektoreita, koodi keskittyy silmukan korkean tason tavoitteeseen. Tämä abstrahoi pois tavanomaista koodia, jolloin tämän koodin ainutlaatuiset käsitteet — kuten suodatusehto, jonka jokaisen iteraattorin elementin täytyy läpäistä — erottuvat paremmin.

Ovatko kaksi toteutusta todella vastaavia? Intuitiivinen oletus saattaa olla, että matalamman tason silmukka on nopeampi. Puhutaan suorituskyvystä.

Suorituskyvyn vertailu: silmukat vs. iteraattorit

Suorituskyky: silmukat vs. iteraattorit

Päättääksesi, käytätkö silmukoita vai iteraattoreita, sinun täytyy tietää, kumpi toteutus on nopeampi: search-funktion versio eksplisiittisellä for-silmukalla vai versio iteraattoreilla.

Suoritimme vertailun lataamalla koko Sir Arthur Conan Doylen teoksen The Adventures of Sherlock Holmes String-merkkijonoon ja etsimällä siitä sanan the. Tässä ovat tulokset for-silmukkaa käyttävästä ja iteraattoreita käyttävästä search-versiosta:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

Molemmat toteutukset ovat suorituskyvyltään samanlaisia! Emme selitä vertailukoodia tässä, koska tarkoitus ei ole todistaa kahden version vastaavuutta vaan saada yleinen käsitys siitä, miten nämä kaksi toteutusta vertautuvat suorituskyvyn kannalta.

Kattavampaa vertailua varten kannattaa testata erilaisia tekstejä eri kokoluokissa contents-parametrina, erilaisia sanoja ja eripituisia sanoja query-parametrina sekä kaikenlaisia muita variaatioita. Pointti on tämä: iteraattorit ovat korkean tason abstraktio, mutta ne käännetään suunnilleen samaan koodiin kuin jos olisit kirjoittanut matalan tason koodin itse. Iteraattorit ovat yksi Rustin nollakustannusisten abstraktioiden muoto; tämä tarkoittaa, että abstraktion käyttö ei aiheuta ylimääräistä ajonaikaista kuormitusta. Tämä on analoginen siihen, miten C++:n alkuperäinen suunnittelija ja toteuttaja Bjarne Stroustrup määrittelee nollaylikuormituksen vuoden 2012 ETAPS-avainpuheessaan ”Foundations of C++”:

In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

Monissa tapauksissa iteraattoreita käyttävä Rust-koodi käännetään samaan konekieleen, jonka kirjoittaisit käsin. Optimoinnit, kuten silmukan purkaminen ja taulukon indeksoinnin rajatarkistusten poistaminen, pätevät ja tekevät syntyneestä koodista erittäin tehokasta. Nyt kun tiedät tämän, voit käyttää iteraattoreita ja sulkeisia pelotta! Ne tekevät koodista korkeamman tason näköistä, mutta eivät aiheuta suorituskyvyn rangaistusta.

Yhteenveto

Sulkeiset ja iteraattorit ovat Rust-ominaisuuksia, jotka ovat saaneet vaikutteita funktionaalisista ohjelmointikielistä. Ne auttavat ilmaisemaan korkean tason ideoita selkeästi matalan tason suorituskyvyllä. Sulkeisten ja iteraattorien toteutukset on suunniteltu niin, etteivät ne vaikuta ajonaikaiseen suorituskykyyn. Tämä on osa Rustin tavoitetta tarjota nollakustannuksisia abstraktioita.

Nyt kun olemme parantaneet I/O-projektimme ilmaisukykyä, katsotaan lisää cargo-ominaisuuksia, jotka auttavat jakamaan projektin maailmalle.

Lisää Cargo:sta ja Crates.io:sta

Tähän mennessä olemme käyttäneet vain Cargon perusominaisuuksia koodin rakentamiseen, suorittamiseen ja testaamiseen, mutta se pystyy paljon enempään. Tässä luvussa käsittelemme joitakin sen edistyneempiä ominaisuuksia ja näytämme, kuinka voit:

  • Mukauttaa rakentamista julkaisuprofiilien avulla
  • Julkaista kirjastoja crates.io-palvelussa
  • Järjestää suuria projekteja työtilojen avulla
  • Asentaa binääritiedostoja crates.io-palvelusta
  • Laajentaa Cargoa mukautetuilla komennoilla

Cargo tarjoaa vielä enemmän toiminnallisuuksia kuin mitä käsittelemme tässä luvussa, joten kaikista sen ominaisuuksista saat lisätietoa sen dokumentaatiosta.

Rakenteiden mukauttaminen julkaisuprofiileilla

Rakennusten mukauttaminen julkaisuprofiilien avulla

Rustissa julkaisuprofiilit ovat ennalta määriteltyjä ja muokattavissa olevia profiileja, joilla on erilaisia asetuksia. Näiden avulla ohjelmoija voi hallita paremmin koodin kääntämiseen liittyviä valintoja. Jokainen profiili määritellään itsenäisesti muista.

Cargo sisältää kaksi pääprofiilia: dev-profiilin, jota Cargo käyttää suoritettaessa cargo build, ja release-profiilin, jota Cargo käyttää suoritettaessa cargo build --release. dev-profiili on määritelty kehitykseen sopivilla oletusasetuksilla, kun taas release-profiililla on hyvät oletusasetukset julkaisua varten.

Nämä profiilinimet voivat olla tuttuja käännöksen tulosteesta:

$ cargo build
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
    Finished `release` profile [optimized] target(s) in 0.32s

dev ja release ovat siis eri profiileja, joita kääntäjä käyttää.

Cargo määrittää oletusasetukset jokaiselle profiilille, jos projektin Cargo.toml-tiedostoon ei ole lisätty erillisiä [profile.*]-osioita. Lisäämällä [profile.*]-osiot haluamillesi profiileille voit muuttaa niiden asetuksia. Esimerkiksi tässä ovat oletusarvot opt-level-asetukselle dev- ja release-profiileissa:

Tiedostonimi: Cargo.toml

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

opt-level-asetus määrittää, kuinka paljon Rust optimoi koodia, asteikolla 0–3. Optimointien lisääminen pidentää käännösaikaa, joten kehitysvaiheessa, kun käännöksiä suoritetaan usein, on parempi käyttää vähemmän optimointeja, jotta kääntäminen on nopeampaa, vaikka suorituskyky ei olisi paras mahdollinen. Tästä syystä dev-profiilin oletusarvo opt-level-asetukselle on 0. Kun koodi on valmis julkaistavaksi, kannattaa panostaa pidempään käännösaikaan, koska käännetty ohjelma suoritetaan useita kertoja. Siksi release-profiilin oletusarvo opt-level-asetukselle on 3.

Voit ohittaa oletusasetuksen lisäämällä uuden arvon Cargo.toml-tiedostoon. Esimerkiksi jos haluat käyttää optimointitasoa 1 kehitysprofiilissa, voit lisätä seuraavat rivit:

Tiedostonimi: Cargo.toml

[profile.dev]
opt-level = 1

Tämä korvaa oletusarvon 0. Nyt, kun suoritat cargo build, Cargo käyttää dev-profiilin oletusasetuksia, mutta soveltaa myös omaa muutostasi opt-level-asetukseen. Koska opt-level on asetettu arvoon 1, Cargo käyttää enemmän optimointeja kuin oletusarvo, mutta ei yhtä paljon kuin julkaisurakenteessa.

Täydellinen luettelo kunkin profiilin asetusvaihtoehdoista ja oletusarvoista löytyy Cargon dokumentaatiosta.

Craten julkaiseminen Crates.io:hon

Kirjaston julkaiseminen Crates.io-palveluun

Olemme käyttäneet crates.io -palvelun paketteja projektiemme riippuvuuksina, mutta voitte myös jakaa koodianne muiden kanssa julkaisemalla omat pakettinne. crates.io -palvelun crate-rekisteri jakaa pakettienne lähdekoodin, joten se isännöi pääasiassa avoimen lähdekoodin koodia.

Rustilla ja Cargolla on ominaisuuksia, jotka tekevät julkaistusta paketistanne helpomman löytää ja käyttää. Käsittelemme seuraavaksi joitakin näistä ominaisuuksista ja selitämme sitten, miten paketti julkaistaan.

Hyödyllisten dokumentaatiokommenttien kirjoittaminen

Pakettienne tarkka dokumentointi auttaa muita käyttäjiä tietämään, miten ja milloin niitä käytetään, joten dokumentaation kirjoittamiseen kannattaa panostaa aikaa. Luvussa 3 käsittelimme, miten Rust-koodia kommentoidaan kahdella kauttaviivalla //. Rustissa on myös erityinen kommenttityyppi dokumentaatiota varten, jota kutsutaan kätevästi dokumentaatiokommentiksi (documentation comment), ja joka tuottaa HTML-dokumentaation. HTML näyttää dokumentaatiokommenttien sisällön julkisille API-kohteille, jotka on tarkoitettu ohjelmoijille, jotka haluavat tietää, miten käyttää crateanne, ei sitä, miten crateanne toteutetaan.

Dokumentaatiokommentit käyttävät kolmea kauttaviivaa /// kahden sijaan ja tukevat Markdown-merkintää tekstin muotoiluun. Sijoittakaa dokumentaatiokommentit juuri ennen dokumentoitavaa kohdetta. Listausta 14-1 näyttää dokumentaatiokommentit add_one-funktiolle my_crate-nimisessä cratessa.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-01/src/lib.rs}}
Listing 14-1: Dokumentaatiokommentti funktiolle

Tässä annamme kuvauksen siitä, mitä add_one-funktio tekee, aloitamme osion otsikolla Examples ja annamme sitten koodin, joka demonstroi add_one-funktion käyttöä. Voimme tuottaa HTML-dokumentaation tästä dokumentaatiokommentista ajamalla cargo doc -komennon. Tämä komento ajaa Rustin mukana jaeltavan rustdoc-työkalun ja sijoittaa tuotetun HTML-dokumentaation target/doc-hakemistoon.

Kätevyyttä varten cargo doc --open -komennon ajaminen rakentaa nykyisen craten dokumentaation HTML-version (sekä kaikkien craten riippuvuuksien dokumentaation) ja avaa tuloksen verkkoselaimessa. Siirtykää add_one-funktioon ja näette, miten dokumentaatiokommenttien teksti renderöidään, kuten kuvassa 14-1:

Rendered HTML documentation for the `add_one` function of `my_crate`

Kuva 14-1: HTML-dokumentaatio add_one-funktiolle

Yleisesti käytetyt osiot

Käytimme # Examples -Markdown-otsikkoa listauksessa 14-1 luodaksemme HTML:ään osion otsikolla ”Examples”. Tässä on joitakin muita osioita, joita crate-kirjoittajat käyttävät yleisesti dokumentaatiossaan:

  • Panics: Tilanteet, joissa dokumentoitu funktio voi panikoida. Funktion kutsujien, jotka eivät halua ohjelmansa panikoivan, tulisi varmistaa, etteivät he kutsu funktiota näissä tilanteissa.
  • Errors: Jos funktio palauttaa Result-arvon, erilaisten virheiden kuvaaminen ja ehtojen, jotka voivat aiheuttaa näiden virheiden palauttamisen, kuvaus voi olla hyödyllistä kutsujille, jotta he voivat kirjoittaa koodia eri virhetyyppien käsittelyyn eri tavoin.
  • Safety: Jos funktion kutsuminen on unsafe (käsittelemme turvattomuutta luvussa 20), pitäisi olla osio, joka selittää, miksi funktio on turvaton ja kattaa invariantit, joiden kutsujien odotetaan ylläpitävän.

Useimmat dokumentaatiokommentit eivät tarvitse kaikkia näitä osioita, mutta tämä on hyvä tarkistuslista muistuttamaan koodinne näkökulmista, joista käyttäjät ovat kiinnostuneita.

Dokumentaatiokommentit testeinä

Esimerkkikoodilohkojen lisääminen dokumentaatiokommentteihinne voi auttaa demonstroimaan kirjastonne käyttöä, ja siitä on lisähyöty: cargo test -komennon ajaminen ajaa dokumentaatiosi koodiesimerkit testeinä! Mikään ei ole parempaa kuin dokumentaatio esimerkein. Mutta mikään ei ole pahempaa kuin esimerkit, jotka eivät toimi, koska koodi on muuttunut dokumentaation kirjoittamisen jälkeen. Jos ajamme cargo test -komennon listauksen 14-1 add_one-funktion dokumentaatiolla, näemme testituloksissa osion, joka näyttää tältä:

   Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

Nyt jos muutamme joko funktiota tai esimerkkiä niin, että esimerkin assert_eq! panikoi, ja ajamme cargo test -komennon uudelleen, näemme, että dokumentaatiotestit havaitsevat esimerkin ja koodin olevan epäsynkassa!

Sisältävän kohteen kommentit

//!-tyylinen dokumentaatiokommentti lisää dokumentaation kommentteja sisältävälle kohteelle sen sijaan, että kommenttien jälkeen tuleville kohteille. Käytämme näitä dokumentaatiokommentteja tyypillisesti crate-juuren tiedostossa (käytännössä src/lib.rs) tai moduulin sisällä dokumentoidaksemme craten tai moduulin kokonaisuudessaan.

Esimerkiksi lisätäksemme dokumentaation, joka kuvaa my_crate-craten tarkoitusta ja joka sisältää add_one-funktion, lisäämme dokumentaatiokommentit, jotka alkavat //!-merkinnällä, src/lib.rs-tiedoston alkuun, kuten listauksessa 14-2:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-02/src/lib.rs:here}}
Listing 14-2: Dokumentaatio my_crate-cratelle kokonaisuudessaan

Huomaa, ettei //!-merkinnällä alkavan viimeisen rivin jälkeen ole mitään koodia. Koska aloitimme kommentit ///-merkinnän sijaan //!-merkinnällä, dokumentoimme kommenttia sisältävää kohdetta sen sijaan, että dokumentoisimme tämän kommentin jälkeen tulevaa kohdetta. Tässä tapauksessa kyseinen kohde on src/lib.rs-tiedosto, joka on crate-juuri. Nämä kommentit kuvaavat koko craten.

Kun ajamme cargo doc --open -komennon, nämä kommentit näytetään my_crate-craten dokumentaation etusivulla craten julkisten kohteiden listan yläpuolella, kuten kuvassa 14-2:

Dokumentaatiokommentit kohteiden sisällä ovat hyödyllisiä erityisesti cratejen ja moduulien kuvaamiseen. Käyttäkää niitä selittämään säilön yleinen tarkoitus auttaaksenne käyttäjiänne ymmärtämään craten rakenteen.

Rendered HTML documentation with a comment for the crate as a whole

Kuva 14-2: Renderöity dokumentaatio my_crate-cratelle, mukaan lukien kommentti, joka kuvaa craten kokonaisuudessaan

Kätevän julkisen API:n vienti

Julkisen API:n rakenne on tärkeä huomioitava asia craten julkaisemisessa. Crateanne käyttävät ihmiset eivät tunne rakennetta yhtä hyvin kuin te, ja heillä saattaa olla vaikeuksia löytää haluamiaan osia, jos cratellanne on suuri moduulihierarkia.

Luvussa 7 käsittelimme, miten kohteet tehdään julkisiksi pub-avainsanalla ja tuodaan näkyvyysalueelle use-avainsanalla. Kehityksen aikana teille järkevä rakenne ei kuitenkaan välttämättä ole kätevä käyttäjillenne. Saatatte haluta järjestää struct-rakenteet hierarkiaan, jossa on useita tasoja, mutta sitten ihmiset, jotka haluavat käyttää syvällä hierarkiassa määrittelemäänne tyyppiä, saattavat vaikeuksien kanssa selvittää, että tyyppi on olemassa. Heitä saattaa myös ärsyttää joutua kirjoittamaan use my_crate::some_module::another_module::UsefulType; use my_crate::UsefulType; -lauseen sijaan.

Hyvä uutinen on, että jos rakenne ei ole kätevä muille kirjastoista käytettäväksi, teidän ei tarvitse järjestellä sisäistä rakennettanne uudelleen: voitte sen sijaan viedä kohteita uudelleen luodaksenne julkisen rakenteen, joka poikkeaa yksityisestä rakenteestanne, käyttämällä pub use -lauseita. Uudelleenvienti ottaa julkisen kohteen yhdestä paikasta ja tekee sen julkiseksi toisessa paikassa, ikään kuin se olisi määritelty tuossa toisessa paikassa.

Esimerkiksi oletetaan, että teimme art-nimisen kirjaston taiteellisten käsitteiden mallintamiseen. Tässä kirjastossa on kaksi moduulia: kinds-moduuli, joka sisältää kaksi enumia nimeltä PrimaryColor ja SecondaryColor, ja utils-moduuli, joka sisältää funktion nimeltä mix, kuten listauksessa 14-3:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-03/src/lib.rs:here}}
Listing 14-3: art-kirjasto, jonka kohteet on järjestetty kinds- ja utils-moduuleihin

Kuva 14-3 näyttää, miltä tämän craten cargo doc -komennolla tuottaman dokumentaation etusivu näyttäisi:

Rendered documentation for the `art` crate that lists the `kinds` and `utils` modules

Kuva 14-3: art-craten dokumentaation etusivu, joka listaa kinds- ja utils-moduulit

Huomaa, että PrimaryColor- ja SecondaryColor-tyypit eivät ole etusivulla, eikä mix-funktio ole. Meidän täytyy klikata kinds- ja utils-moduuleja nähdäksemme ne.

Toinen tästä kirjastosta riippuva crate tarvitsisi use-lauseita, jotka tuovat art-craten kohteet näkyvyysalueelle määrittelemällä nykyisen moduulirakenteen. Listausta 14-4 näyttää esimerkin cratesta, joka käyttää art-craten PrimaryColor- ja mix-kohteita:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-04/src/main.rs}}
Listing 14-4: Crate, joka käyttää art-craten kohteita sen sisäisen rakenteen viennin kautta

Listauksen 14-4 koodin kirjoittajan, joka käyttää art-cratea, täytyi selvittää, että PrimaryColor on kinds-moduulissa ja mix on utils-moduulissa. art-craten moduulirakenne on merkityksellisempi art-cratea kehittäville kuin sitä käyttäville. Sisäinen rakenne ei sisällä hyödyllistä tietoa jollekin, joka yrittää ymmärtää art-craten käyttöä, vaan aiheuttaa hämmennystä, koska sitä käyttävien kehittäjien täytyy selvittää, mistä etsiä, ja heidän täytyy määrittää moduulinimet use-lauseissaan.

Poistaaksemme sisäisen rakenteen julkisesta API:sta voimme muokata listauksen 14-3 art-crate-koodia lisäämällä pub use -lauseita viedäksemme kohteet uudelleen ylimmälle tasolle, kuten listauksessa 14-5:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-05/src/lib.rs:here}}
Listing 14-5: pub use -lauseiden lisääminen kohteiden uudelleenvientiin

cargo doc -komennolla tuottamamme API-dokumentaation etusivu listaa ja linkittää nyt uudelleenviedyt kohteet, kuten kuvassa 14-4, mikä tekee PrimaryColor- ja SecondaryColor-tyypeistä ja mix-funktiosta helpommin löydettäviä.

Rendered documentation for the `art` crate with the re-exports on the front page

Kuva 14-4: art-craten dokumentaation etusivu, joka listaa uudelleenviedyt kohteet

art-craten käyttäjät voivat edelleen nähdä ja käyttää listauksen 14-3 sisäistä rakennetta, kuten listauksessa 14-4 demonstroitu, tai he voivat käyttää listauksen 14-5 kätevämpää rakennetta, kuten listauksessa 14-6:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-06/src/main.rs:here}}
Listing 14-6: Ohjelma, joka käyttää art-craten uudelleenvietyjä kohteita

Tapauksissa, joissa on monia sisäkkäisiä moduuleja, uudelleenviedyt tyyppien vienti ylimmälle tasolle pub use -lauseella voi merkittävästi parantaa craten käyttäjien kokemusta. Toinen yleinen pub use -käyttö on riippuvuuden määritelmien uudelleenvienti nykyisessä cratessa, jotta kyseisen craten määritelmät tulevat osaksi cratenne julkista API:ta.

Hyödyllisen julkisen API-rakenteen luominen on enemmän taidetta kuin tiedettä, ja voitte iteroida löytääksenne käyttäjillenne parhaiten toimivan API:n. pub use -valinta antaa teille joustavuutta craten sisäisen rakenteen suhteen ja erottaa sen sisäisen rakenteen siitä, mitä esitätte käyttäjillenne. Tutustukaa asentamienne cratejen koodiin nähdäksenne, poikkeaako niiden sisäinen rakenne julkisesta API:sta.

Crates.io-tilin luominen

Ennen kuin voitte julkaista crateja, teidän täytyy luoda tili crates.io -palveluun ja hankkia API-tunnus. Tehkää tämä vierailemalla etusivulla osoitteessa crates.io ja kirjautumalla GitHub-tilin kautta. (GitHub-tili on tällä hetkellä vaatimus, mutta sivusto saattaa tukea muita tapoja tilin luomiseen tulevaisuudessa.) Kun olette kirjautuneet sisään, vierailette tilinne asetuksissa osoitteessa https://crates.io/me/ ja haette API-avaimenne. Suorittakaa sitten cargo login -komento ja liittäkää API-avaimenne kehotettaessa, näin:

$ cargo login
abcdefghijklmnopqrstuvwxyz012345

Tämä komento ilmoittaa Cargolle API-tunnuksestanne ja tallentaa sen paikallisesti tiedostoon ~/.cargo/credentials.toml. Huomaa, että tämä tunnus on salaisuus: älkää jakako sitä kenellekään muulle. Jos jaatte sen jostain syystä kenellekään, teidän pitäisi mitätöidä se ja luoda uusi tunnus osoitteessa crates.io.

Metatietojen lisääminen uuteen crateen

Oletetaan, että teillä on crate, jonka haluatte julkaista. Ennen julkaisemista teidän täytyy lisätä metatietoja craten Cargo.toml-tiedoston [package]-osioon.

Cratellanne täytyy olla yksilöllinen nimi. Kun työskentelette craten parissa paikallisesti, voitte nimetä craten miten haluatte. crates.io -palvelun cratenimet jaetaan kuitenkin ensimmäisen tulevan periaatteella. Kun craten nimi on varattu, kukaan muu ei voi julkaista cratea sillä nimellä. Ennen julkaisuyritystä etsikää haluamanne nimi. Jos nimi on jo käytössä, teidän täytyy löytää toinen nimi ja muokata name-kenttää Cargo.toml-tiedoston [package]-osiossa käyttämään uutta nimeä julkaisemista varten, näin:

Tiedostonimi: Cargo.toml

[package]
name = "guessing_game"

Vaikka olisitte valinneet yksilöllisen nimen, kun ajatte cargo publish -komennon julkaistaksenne craten tässä vaiheessa, saatte varoituksen ja sitten virheen:

$ cargo publish
    Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io

Caused by:
  the remote server responded with an error (status 400 Bad Request): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields

Tämä virhe johtuu siitä, että teiltä puuttuu tärkeitä tietoja: kuvaus ja lisenssi ovat pakollisia, jotta ihmiset tietävät, mitä craten tekee ja millä ehdoilla he voivat käyttää sitä. Lisätkää Cargo.toml-tiedostoon kuvaus, joka on vain lause tai kaksi, koska se näkyy craten kanssa hakutuloksissa. license-kenttään teidän täytyy antaa lisenssitunnistearvo. Linux Foundationin Software Package Data Exchange (SPDX) listaa tunnisteet, joita voitte käyttää tähän arvoon. Esimerkiksi määrittääksenne, että olette lisensoineet cratenne MIT-lisenssillä, lisätkää MIT-tunniste:

Tiedostonimi: Cargo.toml

[package]
name = "guessing_game"
license = "MIT"

Jos haluatte käyttää lisenssiä, joka ei esiinny SPDX-listassa, teidän täytyy sijoittaa kyseisen lisenssin teksti tiedostoon, sisällyttää tiedosto projektiinne ja käyttää sitten license-file-kenttää määrittämään kyseisen tiedoston nimi license-avaimen sijaan.

Ohjeet siihen, mikä lisenssi sopii projektiinne, ylittävät tämän kirjan laajuuden. Monet Rust-yhteisön jäsenet lisensoivat projektinsa samalla tavalla kuin Rust käyttämällä kaksoislisenssiä MIT OR Apache-2.0. Tämä käytäntö osoittaa, että voitte myös määrittää useita lisenssitunnisteita erotettuna OR-sanalla, jotta projektillanne on useita lisenssejä.

Kun olette lisänneet yksilöllisen nimen, version, kuvauksen ja lisenssin, julkaisuvalmiin projektin Cargo.toml-tiedosto saattaa näyttää tältä:

Tiedostonimi: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"

[dependencies]

Cargon dokumentaatio kuvaa muuta metatietoa, jonka voitte määrittää varmistaaksenne, että muut löytävät ja käyttävät crateanne helpommin.

Julkaiseminen Crates.io-palveluun

Nyt kun olette luoneet tilin, tallentaneet API-tunnuksenne, valinneet nimen cratelle ja määrittäneet vaaditut metatiedot, olette valmiita julkaisemaan! Craten julkaiseminen lataa tietyn version crates.io -palveluun muiden käytettäväksi.

Olkaa varovaisia, koska julkaisu on pysyvä. Versiota ei voi koskaan korvata, eikä koodia voi poistaa paitsi tietyissä olosuhteissa. Yksi crates.io -palvelun tärkeimmistä tavoitteista on toimia pysyvänä koodiarkistona, jotta kaikkien crates.io -palvelun crateista riippuvien projektien käännökset toimivat edelleen. Versioiden poistamisen salliminen tekisi tämän tavoitteen toteuttamisen mahdottomaksi. Julkaistavien crateversioiden määrälle ei kuitenkaan ole rajaa.

Ajakaa cargo publish -komento uudelleen. Sen pitäisi nyt onnistua:

$ cargo publish
    Updating crates.io index
   Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
    Packaged 6 files, 1.2KiB (895.0B compressed)
   Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
   Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
   Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
    Uploaded guessing_game v0.1.0 to registry `crates-io`
note: waiting for `guessing_game v0.1.0` to be available at registry
`crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
   Published guessing_game v0.1.0 at registry `crates-io`

Onnittelut! Olette nyt jakaneet koodinne Rust-yhteisön kanssa, ja kuka tahansa voi helposti lisätä cratenne riippuvuudekseen projektiinsa.

Olemassa olevan craten uuden version julkaiseminen

Kun olette tehneet muutoksia crateenne ja olette valmiita julkaisemaan uuden version, muutatte Cargo.toml-tiedostossa määritettyä version-arvoa ja julkaisette uudelleen. Käyttäkää semanttisen versionoinnin sääntöjä päättääksenne, mikä sopiva seuraava versionumero on tekemiesi muutosten perusteella. Suorittakaa sitten cargo publish -komento ladataksenne uuden version.

Crateversioiden poistaminen käytöstä Crates.io-palvelussa

Vaikka ette voi poistaa craten aiempia versioita, voitte estää tulevia projekteja lisäämästä niitä uutena riippuvuutena. Tämä on hyödyllistä, kun crateversio on jostain syystä rikki. Tällaisissa tilanteissa Cargo tukee crateversion poistamista käytöstä (yanking).

Version poistaminen käytöstä estää uusia projekteja riippumasta kyseisestä versiosta, mutta sallii kaikkien olemassa olevien projektien, jotka riippuvat siitä, jatkaa. Pohjimmiltaan poistaminen käytöstä tarkoittaa, että kaikki projektit, joilla on Cargo.lock-tiedosto, eivät hajoa, eikä mikään tuleva Cargo.lock-tiedosto käytä poistettua versiota.

Poistaaksenne craten version käytöstä, craten hakemistossa, jonka olette aiemmin julkaisseet, suorittakaa cargo yank -komento ja määrittäkää, minkä version haluatte poistaa käytöstä. Esimerkiksi jos olemme julkaisseet guessing_game-craten version 1.0.1 ja haluamme poistaa sen käytöstä, guessing_game-projektin hakemistossa suorittaisimme:

$ cargo yank --vers 1.0.1
    Updating crates.io index
        Yank guessing_game@1.0.1

Lisäämällä --undo-lipun komentoon voitte myös perua poistamisen käytöstä ja sallia projektien alkaa riippua versiosta uudelleen:

$ cargo yank --vers 1.0.1 --undo
    Updating crates.io index
      Unyank guessing_game@1.0.1

Poistaminen käytöstä ei poista mitään koodia. Se ei esimerkiksi voi poistaa vahingossa ladattuja salaisuuksia. Jos näin tapahtuu, teidän täytyy nollata nämä salaisuudet välittömästi.

Cargo-työtilat

Cargo-työtilat

Luvussa 12 rakensimme paketin, joka sisälsi binääricraten ja kirjastocraten. Projektin kehittyessä kirjastocrate voi kasvaa niin suureksi, että haluatte jakaa pakettinne edelleen useisiin kirjastocrateihin. Cargo tarjoaa ominaisuuden nimeltä työtilat, joka voi auttaa hallitsemaan useita toisiinsa liittyviä paketteja, joita kehitetään rinnakkain.

Työtilan luominen

Työtila on joukko paketteja, jotka jakavat saman Cargo.lock-tiedoston ja tulostushakemiston. Tehdään projekti käyttäen työtilaa—käytämme triviaalia koodia, jotta voimme keskittyä työtilan rakenteeseen. Työtilan voi rakentaa monella tavalla, joten näytämme vain yhden yleisen tavan. Työtilassa on binääri ja kaksi kirjastoa. Binääri, joka tarjoaa päätoiminnallisuuden, riippuu kahdesta kirjastosta. Yksi kirjasto tarjoaa add_one-funktion ja toinen kirjasto add_two-funktion. Nämä kolme cratea ovat osa samaa työtilaa. Aloitamme luomalla uuden hakemiston työtilalle:

$ mkdir add
$ cd add

Seuraavaksi add-hakemistossa luomme Cargo.toml-tiedoston, joka määrittää koko työtilan. Tässä tiedostossa ei ole [package]-osiota. Sen sijaan se alkaa [workspace]-osiolla, jonka avulla voimme lisätä jäseniä työtilaan. Varmistamme myös, että käytämme Cargon uusinta ja parasta resolver-algoritmia työtilassamme asettamalla resolver-asetuksen arvoksi "3":

Tiedostonimi: Cargo.toml

{{#include ../listings/ch14-more-about-cargo/no-listing-01-workspace/add/Cargo.toml}}

Seuraavaksi luomme adder-binääricraten suorittamalla cargo new -komennon add-hakemistossa:

$ cargo new adder
     Created binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

cargo new -komennon suorittaminen työtilan sisällä lisää myös automaattisesti juuri luodun paketin työtilan Cargo.toml-tiedoston [workspace]-määrittelyn members-avaimeen, näin:

{{#include ../listings/ch14-more-about-cargo/output-only-01-adder-crate/add/Cargo.toml}}

Tässä vaiheessa voimme rakentaa työtilan suorittamalla cargo build -komennon. Tiedostot add-hakemistossanne pitäisi näyttää tältä:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Työtilalla on yksi target-hakemisto ylätasolla, johon käännetyt artefaktit sijoitetaan; adder-paketilla ei ole omaa target-hakemistoa. Vaikka suorittaisimme cargo build -komennon adder-hakemiston sisältä, käännetyt artefaktit päätyisivät silti hakemistoon add/target eikä add/adder/target. Cargo rakentaa target-hakemiston työtilassa näin, koska työtilan cratet on tarkoitettu riippuvan toisistaan. Jos jokaisella cratella olisi oma target-hakemisto, jokaisen craten täytyisi kääntää uudelleen jokainen työtilan muista crateista sijoittaakseen artefaktit omaan target-hakemistoonsa. Jakamalla yhden target-hakemiston cratet voivat välttää tarpeettoman uudelleenkääntämisen.

Toisen paketin luominen työtilaan

Seuraavaksi luodaan työtilaan toinen jäsenpaketti ja kutsutaan sitä add_one. Luodaan uusi kirjastocrate nimeltä add_one:

$ cargo new add_one --lib
     Created library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

Ylätason Cargo.toml-tiedosto sisältää nyt add_one-polun members-listassa:

Tiedostonimi: Cargo.toml

{{#include ../listings/ch14-more-about-cargo/no-listing-02-workspace-with-two-crates/add/Cargo.toml}}

add-hakemistossanne pitäisi nyt olla nämä hakemistot ja tiedostot:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Lisätään add_one/src/lib.rs-tiedostoon add_one-funktio:

Tiedostonimi: add_one/src/lib.rs

{{#rustdoc_include ../listings/ch14-more-about-cargo/no-listing-02-workspace-with-two-crates/add/add_one/src/lib.rs}}

Nyt voimme tehdä adder-paketista, jossa on binäärimme, riippuvaisen add_one-paketista, jossa on kirjastomme. Ensin meidän täytyy lisätä polkuriippuvuus add_one-pakettiin tiedostoon adder/Cargo.toml.

Tiedostonimi: adder/Cargo.toml

{{#include ../listings/ch14-more-about-cargo/no-listing-02-workspace-with-two-crates/add/adder/Cargo.toml:6:7}}

Cargo ei oleta, että työtilan cratet riippuvat toisistaan, joten meidän täytyy määritellä riippuvuussuhteet eksplisiittisesti.

Seuraavaksi käytetään add_one-funktiota (add_one-cratesta) adder-cratessa. Avatkaa adder/src/main.rs-tiedosto ja muuttakaa main-funktiota kutsumaan add_one-funktiota, kuten listauksessa 14-7.

Filename: adder/src/main.rs
{{#rustdoc_include ../listings/ch14-more-about-cargo/listing-14-07/add/adder/src/main.rs}}
Listing 14-7: add_one-kirjastocraten käyttö adder-cratessa

Rakennetaan työtila suorittamalla cargo build -komento ylätason add-hakemistossa!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s

Suorittaaksemme binääricraten add-hakemistosta voimme määrittää, minkä paketin työtilassa haluamme suorittaa käyttämällä -p-argumenttia ja paketin nimeä komennolla cargo run:

$ cargo run -p adder
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Tämä suorittaa koodin tiedostossa adder/src/main.rs, joka riippuu add_one-cratesta.

Ulkoisen paketin riippuvuus

Huomaa, että työtilassa on vain yksi Cargo.lock-tiedosto ylätasolla sen sijaan, että jokaisella craten hakemistolla olisi oma Cargo.lock. Tämä varmistaa, että kaikki cratet käyttävät samaa versiota kaikista riippuvuuksista. Jos lisäämme rand-paketin tiedostoihin adder/Cargo.toml ja add_one/Cargo.toml, Cargo ratkaisee molemmat yhdeksi rand-versioksi ja tallentaa sen yhteen Cargo.lock-tiedostoon. Kaikkien työtilan cratejen saman riippuvuuksien käyttö tarkoittaa, että cratet ovat aina yhteensopivia toistensa kanssa. Lisätään rand-crate [dependencies]-osioon tiedostoon add_one/Cargo.toml voidaksemme käyttää rand-cratea add_one-cratessa:

Tiedostonimi: add_one/Cargo.toml

{{#include ../listings/ch14-more-about-cargo/no-listing-03-workspace-with-external-dependency/add/add_one/Cargo.toml:6:7}}

Voimme nyt lisätä use rand; tiedostoon add_one/src/lib.rs ja rakentaa koko työtilan suorittamalla cargo build -komennon add-hakemistossa, mikä tuo mukaan ja kääntää rand-craten. Saamme yhden varoituksen, koska emme viittaa näkyviin tuomaamme rand-crateen:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.10.1
   --snip--
   Compiling rand v0.10.1
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

Ylätason Cargo.lock sisältää nyt tietoa add_one-craten riippuvuudesta rand-crateen. Vaikka rand-cratea käytetään jossain työtilassa, emme voi käyttää sitä muissa työtilan crateissa, ellemme lisää rand-cratea myös niiden Cargo.toml-tiedostoihin. Esimerkiksi jos lisäämme use rand; tiedostoon adder/src/main.rs adder-paketille, saamme virheen:

$ cargo build
  --snip--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

Korjataksemme tämän muokkaa adder-paketin Cargo.toml-tiedostoa ja ilmoittakaa, että rand on riippuvuus myös sille. adder-paketin rakentaminen lisää rand-craten adder-paketin riippuvuuksien listaan tiedostossa Cargo.lock, mutta yhtään lisäkopiota rand-cratesta ei ladata. Cargo varmistaa, että jokainen crate jokaisessa työtilan paketissa, joka käyttää rand-pakettia, käyttää samaa versiota niin kauan kuin ne määrittävät yhteensopivia versioita rand-cratesta, säästäen tilaa ja varmistaen, että työtilan cratet ovat yhteensopivia toistensa kanssa.

Jos työtilan cratet määrittävät yhteensopimattomia versioita samasta riippuvuudesta, Cargo ratkaisee jokaisen niistä, mutta yrittää silti ratkaista mahdollisimman vähän versioita.

Testin lisääminen työtilaan

Lisäparannuksena lisätään testi add_one::add_one-funktiolle add_one-cratessa:

Tiedostonimi: add_one/src/lib.rs

{{#rustdoc_include ../listings/ch14-more-about-cargo/no-listing-04-workspace-with-tests/add/add_one/src/lib.rs}}

Suorittakaa nyt cargo test ylätason add-hakemistossa. cargo test -komennon suorittaminen tällaisessa työtilassa suorittaa testit kaikille työtilan crateille:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Tulosteen ensimmäinen osa osoittaa, että add_one-craten it_works-testi läpäisi. Seuraava osa osoittaa, että adder-cratessa ei löytynyt testejä, ja viimeinen osa osoittaa, että add_one-cratessa ei löytynyt dokumentaatiotestejä.

Voimme myös suorittaa testit yhdelle tietylle cratelle työtilassa ylätason hakemistosta käyttämällä -p-lippua ja määrittämällä craten nimen, jota haluamme testata:

$ cargo test -p add_one
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Tämä tuloste osoittaa, että cargo test suoritti vain add_one-craten testit eikä suorittanut adder-craten testejä.

Jos julkaisette työtilan cratet crates.io -palveluun, jokainen työtilan crate on julkaistava erikseen. Kuten cargo test, voimme julkaista tietyn craten työtilastamme käyttämällä -p-lippua ja määrittämällä craten nimen, jonka haluamme julkaista.

Lisäharjoitukseksi lisätkää tähän työtilaan add_two-crate samalla tavalla kuin add_one-crate!

Kun projektinne kasvaa, harkitkaa työtilan käyttämistä: on helpompi ymmärtää pienempiä yksittäisiä komponentteja kuin yhtä suurta koodipalaa. Lisäksi cratet työtilassa helpottavat cratejen välistä koordinointia, jos niitä muutetaan usein samaan aikaan.

Binäärien asentaminen Crates.io:sta cargo install-komennolla

Binäärien asentaminen cargo install -komennolla

cargo install -komento mahdollistaa binäärikääntäjien asentamisen ja käyttämisen paikallisesti. Tätä ei ole tarkoitettu korvaamaan järjestelmän pakettienhallintaa, vaan se on kätevä tapa Rust-kehittäjille asentaa työkaluja, joita muut ovat jakaneet crates.io-palvelussa. Huomaa, että voit asentaa vain paketteja, joilla on binääritavoite.

Binääritavoite tarkoittaa ajettavaa ohjelmaa, joka syntyy, jos paketilla on src/main.rs-tiedosto tai jokin muu binääriksi määritelty tiedosto. Kirjastotavoitteet eivät ole suoraan ajettavissa, vaan ne on tarkoitettu käytettäväksi osana muita ohjelmia. Usein paketin README-tiedostossa on tietoa siitä, onko kyseessä kirjasto, binääri vai molemmat.

Kaikki cargo install -komennolla asennetut binäärit tallennetaan asennuspolun bin-hakemistoon. Jos asensit Rustin rustup.rs:n kautta etkä ole muuttanut asetuksia, tämä hakemisto on $HOME/.cargo/bin. Varmista, että tämä hakemisto on mukana järjestelmäsi $PATH-muuttujassa, jotta voit suorittaa asentamasi ohjelmat.

Esimerkiksi luvussa 12 mainittiin Rustilla toteutettu grep-työkalu nimeltä ripgrep, jota voidaan käyttää tiedostojen hakemiseen. ripgrep voidaan asentaa seuraavasti:

$ cargo install ripgrep
    Updating crates.io index
  Downloaded ripgrep v14.1.1
  Downloaded 1 crate (213.6 KB) in 0.40s
  Installing ripgrep v14.1.1
--snip--
   Compiling grep v0.3.2
    Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
  Installing ~/.cargo/bin/rg
   Installed package `ripgrep v14.1.1` (executable `rg`)

Lähes viimeinen rivi näyttää asennetun binäärin sijainnin ja nimen, joka ripgrep-tapauksessa on rg. Jos asennushakemisto on $PATH-muuttujassa, kuten aiemmin mainittiin, voit suorittaa komennon rg --help ja alkaa käyttää tätä nopeaa Rust-pohjaista hakutyökalua!

Cargon laajentaminen mukautetuilla komennoilla

Cargon laajentaminen mukautetuilla komennoilla

Cargo on suunniteltu niin, että voit laajentaa sitä uusilla alikomennoilla ilman, että sinun tarvitsee muokata itse Cargoa. Jos jokin binääritiedosto sijaitsee $PATH-hakemistossasi ja sen nimi on cargo-jotain, voit suorittaa sen kuten minkä tahansa Cargon alikomennon käyttämällä komentoa cargo jotain. Tällaiset mukautetut komennot näkyvät myös, kun suoritat cargo --list.

Mahdollisuus asentaa laajennuksia cargo install -komennolla ja käyttää niitä samalla tavalla kuin sisäänrakennettuja Cargo-työkaluja on erittäin kätevä osa Cargon suunnittelua!

Yhteenveto

Koodin jakaminen Cargon ja crates.io-palvelun kautta on yksi Rust-ekosysteemin vahvuuksista. Rustin standardikirjasto on pieni ja vakaa, mutta erillisten pakettien jakaminen, käyttöönotto ja parantaminen voidaan tehdä riippumatta itse kielen kehityksestä.

Älä epäröi jakaa koodia, joka on sinulle hyödyllistä, crates.io-palvelussa – todennäköisesti se on hyödyllistä myös muille!

Älykkäät osoittimet

Osoitin on yleinen käsite muuttujalle, joka sisältää muistiosoitteen. Tämä osoite viittaa johonkin muuhun dataan tai ”osoittaa” sitä. Rustin yleisin osoitintyyppi on viite, josta opit luvussa 4. Viitteet merkitään &-symbolilla ja lainaavat arvoa, johon ne viittaavat. Niillä ei ole erityisiä ominaisuuksia datan viittaamisen lisäksi, eikä niihin liity ylimääräistä kuormitusta.

Älykkäät osoittimet puolestaan ovat datarakenteita, jotka käyttäytyvät osoittimien tavoin mutta sisältävät myös lisämetatietoa ja -ominaisuuksia. Älykkäiden osoittimien käsite ei ole Rustille ainutlaatuinen: ne ovat peräisin C++:sta ja esiintyvät muissakin kielissä. Rustissa on standardikirjastossa useita älykkäitä osoittimia, jotka tarjoavat toiminnallisuutta viitteiden tarjoaman lisäksi. Tutustumme yleiseen käsitteeseen tarkastelemalla muutamia eri esimerkkejä älykkäistä osoittimista, mukaan lukien viitelaskennallinen älykäs osoitintyyppi. Tämä osoitin mahdollistaa usean omistajan sallimisen pitämällä kirjaa omistajien määrästä ja vapauttamalla datan, kun omistajia ei enää ole.

Rustissa omistajuuden ja lainauksen käsitteiden vuoksi viitteiden ja älykkäiden osoittimien välillä on lisäero: vaikka viitteet lainaavat dataa, älykkäät osoittimet omistavat usein datan, johon ne viittaavat.

Älykkäät osoittimet toteutetaan yleensä rakenteina. Tavallisesta rakenteesta poiketen älykkäät osoittimet toteuttavat Deref- ja Drop-traitit. Deref-traitin avulla älykkään osoittimen instanssi voi käyttäytyä viitteen tavoin, jolloin koodisi voi toimia sekä viitteiden että älykkäiden osoittimien kanssa. Drop-traitin avulla voit mukauttaa koodia, joka suoritetaan, kun älykkään osoittimen instanssi poistuu näkyvyysalueeltaan. Tässä luvussa käsittelemme molempia traitteja ja selitämme, miksi ne ovat tärkeitä älykkäille osoittimille.

Koska älykkäiden osoittimien malli on yleinen suunnittelumalli, jota käytetään Rustissa usein, tämä luku ei kata kaikkia olemassa olevia älykkäitä osoittimia. Monilla kirjastoilla on omia älykkäitä osoittimiaan, ja voit kirjoittaa omasikin. Käsittelemme yleisimmät standardikirjaston älykkäät osoittimet:

  • Box<T> arvojen allokointiin pinomuistiin
  • Rc<T>, viitelaskennallinen tyyppi, joka mahdollistaa usean omistajuuden
  • Ref<T> ja RefMut<T>, joihin pääsee RefCell<T>-tyypin kautta; tyyppi pakottaa lainaussäännöt ajonaikana käännösaikan sijaan

Lisäksi käsittelemme sisäisen muuttuvuuden mallin, jossa muuttumaton tyyppi tarjoaa rajapinnan sisäisen arvon muuttamiseen. Käsittelemme myös viitesyklejä: miten ne voivat vuotaa muistia ja miten niitä voi estää.

Aloitetaan!

Box<T>:n käyttö osoittamaan pinomuistin tietoihin

Box<T>-tyypin käyttö keolla olevan datan osoittamiseen

Yksinkertaisin älykäs osoitin on box, jonka tyyppi kirjoitetaan Box<T>. Boxien avulla voit tallentaa datan keolle pinon sijaan. Pinossa säilyy osoitin keolla olevaan dataan. Katso luku 4, jos haluat kerrata pinon ja keon eron.

Boxeilla ei ole suorituskykyhaittaa muuta kuin se, että niiden data tallennetaan keolle pinon sijaan. Niillä ei myöskään ole juuri muita erityisominaisuuksia. Käytät niitä useimmiten seuraavissa tilanteissa:

  • Kun sinulla on tyyppi, jonka kokoa ei voida tietää käännösaikana, ja haluat käyttää kyseisen tyypin arvoa kontekstissa, joka vaatii tarkan koon
  • Kun sinulla on suuri määrä dataa ja haluat siirtää omistajuuden varmistaen, ettei dataa kopioida siirron yhteydessä
  • Kun haluat omistaa arvon ja sinulle riittää, että se on tietyn traitin toteuttava tyyppi, etkä välitä sen tarkasta tyypistä

Ensimmäistä tilannetta käsitellään osiossa ”Rekursiivisten tyyppien mahdollistaminen boxeilla”. Toisessa tapauksessa suuren datamäärän omistajuuden siirtäminen voi kestää kauan, koska dataa kopioidaan pinossa. Tämän tilanteen suorituskyvyn parantamiseksi voimme tallentaa suuren datamäärän keolle boxiin. Tällöin pinossa kopioidaan vain pieni määrä osoitindataa, kun taas sen viittaama data pysyy yhdessä paikassa keolla. Kolmatta tapausta kutsutaan trait-olioksi, ja ”Trait-objektien käyttö jaettuun käyttäytymiseen abstrahoimiseen” luvussa 18 on omistettu juuri tälle aiheelle. Siis tässä oppimasi soveltuu uudelleen siinä osiossa!

Datan tallentaminen keolle

Ennen kuin käsittelemme Box<T>:n käyttötapaa keon tallennukseen, käymme läpi syntaksin ja sen, miten Box<T>:n sisällä oleviin arvoihin viitataan.

Listauksessa 15-1 näytetään, miten boxilla tallennetaan i32-arvo keolle.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-01/src/main.rs}}
}
Listing 15-1: i32-arvon tallentaminen keolle boxin avulla

Määrittelemme muuttujan b siten, että sen arvo on Box, joka osoittaa arvoon 5, joka on allokoitu keolle. Tämä ohjelma tulostaa b = 5; tässä tapauksessa pääsemme boxin dataan käsiksi samalla tavalla kuin jos data olisi pinossa. Kuten minkä tahansa omistetun arvon kohdalla, kun box poistuu näkyvyysalueeltaan — kuten b tekee main-funktion lopussa — se vapautetaan. Vapautus koskee sekä boxia (joka on pinossa) että sen osoittamaa dataa (joka on keolla).

Yhden arvon sijoittaminen keolle ei ole kovin hyödyllistä, joten et käytä boxeja yksinään tällä tavalla usein. Arvojen, kuten yksittäisen i32:n, pitäminen pinossa, missä ne oletusarvoisesti tallennetaan, on sopivampaa useimmissa tilanteissa. Katsotaan seuraavaksi tapausta, jossa boxien avulla voimme määritellä tyyppejä, joita emme voisi määritellä ilman boxeja.

Rekursiivisten tyyppien mahdollistaminen boxeilla

Rekursiivisen tyypin arvo voi sisältää osanaan toisen saman tyypin arvon. Rekursiiviset tyypit aiheuttavat ongelman, koska Rustin on tiedettävä käännösaikana, kuinka paljon tilaa tyyppi vie. Rekursiivisten tyyppien arvojen sisäkkäisyys voisi kuitenkin teoriassa jatkua äärettömästi, joten Rust ei voi tietää, kuinka paljon tilaa arvo tarvitsee. Koska boxeilla on tunnettu koko, voimme mahdollistaa rekursiiviset tyypit lisäämällä boxin rekursiivisen tyypin määrittelyyn.

Esimerkkinä rekursiivisesta tyypistä tutustutaan cons-listaan. Tämä on funktionaalisten ohjelmointikielten yleinen tietorakenne. Määrittelemämme cons-listatyyppi on yksinkertainen paitsi rekursion osalta; siksi tässä esimerkissä käsiteltävät käsitteet ovat hyödyllisiä aina, kun kohtaat monimutkaisempia tilanteita, joissa on rekursiivisia tyyppejä.

Cons-listan ymmärtäminen

Cons-lista on Lisp-ohjelmointikielestä ja sen murreista peräisin oleva tietorakenne, joka koostuu sisäkkäisistä pareista ja on Lispin versio linkitetystä listasta. Sen nimi tulee Lispin cons-funktiosta (lyhenne sanasta construct function, rakennusfunktio), joka muodostaa uuden parin kahdesta argumentistaan. Kutsumalla cons:ia parille, joka koostuu arvosta ja toisesta parista, voimme rakentaa rekursiivisista pareista koostuvia cons-listoja.

Esimerkiksi tässä on pseudokoodiesitys cons-listasta, joka sisältää listan 1, 2, 3 siten, että jokainen pari on sulkeissa:

(1, (2, (3, Nil)))

Jokainen cons-listan alkio sisältää kaksi elementtiä: nykyisen alkion arvon ja seuraavan alkion. Listan viimeinen alkio sisältää vain arvon nimeltä Nil ilman seuraavaa alkioita. Cons-lista syntyy kutsumalla cons-funktiota rekursiivisesti. Rekursion perustapauksen vakiintunut nimi on Nil. Huomaa, että tämä ei ole sama kuin luvussa 6 käsitelty ”null”- tai ”nil”-käsite, joka tarkoittaa virheellistä tai puuttuvaa arvoa.

Cons-lista ei ole Rustissa yleisesti käytetty tietorakenne. Useimmiten, kun Rustissa on lista alkioita, Vec<T> on parempi valinta. Muut, monimutkaisemmat rekursiiviset tietotyypit ovat hyödyllisiä eri tilanteissa, mutta aloittamalla cons-listasta tässä luvussa voimme tutkia, miten boxien avulla voidaan määritellä rekursiivinen tietotyyppi ilman liikaa häiriötekijöitä.

Listauksessa 15-2 on enum-määrittely cons-listalle. Huomaa, että tämä koodi ei vielä käänny, koska List-tyypillä ei ole tunnettua kokoa, kuten demonstroimme.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-02/src/main.rs:here}}
Listing 15-2: Ensimmäinen yritys määritellä enum, joka edustaa i32-arvojen cons-listatietorakennetta

Huom: Toteutamme tässä esimerkissä cons-listan, joka sisältää vain i32-arvoja. Olisimme voineet toteuttaa sen geneerisesti, kuten käsittelimme luvussa 10, ja määritellä cons-listatyypin, joka voi tallentaa minkä tahansa tyyppisiä arvoja.

List-tyypin käyttö listan 1, 2, 3 tallentamiseen näyttäisi listauksen 15-3 koodilta.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-03/src/main.rs:here}}
Listing 15-3: List-enumin käyttö listan 1, 2, 3 tallentamiseen

Ensimmäinen Cons-arvo sisältää 1:n ja toisen List-arvon. Tämä List-arvo on toinen Cons-arvo, joka sisältää 2:n ja toisen List-arvon. Tämä List-arvo on vielä yksi Cons-arvo, joka sisältää 3:n ja List-arvon, joka on lopulta Nil, ei-rekursiivinen variantti, joka merkitsee listan loppua.

Jos yritämme kääntää listauksen 15-3 koodin, saamme listauksessa 15-4 näytetyn virheen.

{{#include ../listings/ch15-smart-pointers/listing-15-03/output.txt}}
Listing 15-4: Virhe, jonka saamme yrittäessämme määritellä rekursiivisen enumin

Virhe kertoo, että tällä tyypillä on ”ääretön koko”. Syy on, että olemme määritelleet List:in variantilla, joka on rekursiivinen: se sisältää suoraan toisen saman tyypin arvon. Tämän vuoksi Rust ei pysty selvittämään, kuinka paljon tilaa List-arvon tallentaminen vaatii. Puretaan seuraavaksi, miksi saamme tämän virheen. Ensin katsomme, miten Rust päättää, kuinka paljon tilaa ei-rekursiivisen tyypin arvo tarvitsee.

Ei-rekursiivisen tyypin koon laskeminen

Muista Message-enum, jonka määrittelimme listauksessa 6-2, kun käsittelimme enum-määrittelyjä luvussa 6:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-02/src/main.rs:here}}
}

Selvittääkseen, kuinka paljon tilaa Message-arvon varaamiseen tarvitaan, Rust käy läpi jokaisen variantin ja katsoo, mikä niistä tarvitsee eniten tilaa. Rust näkee, että Message::Quit ei tarvitse lainkaan tilaa, Message::Move tarvitsee tilaa kahdelle i32-arvolle, ja niin edelleen. Koska vain yhtä varianttia käytetään kerrallaan, enimmäistila, jota Message-arvo tarvitsee, on suurimman variantin vaatima tila.

Vertaa tätä siihen, mitä tapahtuu, kun Rust yrittää selvittää, kuinka paljon tilaa rekursiivinen tyyppi, kuten listauksen 15-2 List-enum, tarvitsee. Kääntäjä aloittaa Cons-variantista, joka sisältää i32-tyyppisen arvon ja List-tyyppisen arvon. Siksi Cons tarvitsee tilaa, joka on yhtä suuri kuin i32:n koko plus List:in koko. Selvittääkseen, kuinka paljon muistia List-tyyppi tarvitsee, kääntäjä katsoo variantteja alkaen Cons-variantista. Cons-variantti sisältää i32-tyyppisen arvon ja List-tyyppisen arvon, ja tämä prosessi jatkuu äärettömästi, kuten kuvassa 15-1.

Ääretön Cons-lista: suorakulmio, jossa lukee 'Cons' ja joka on jaettu kahteen pienempään suorakulmioon. Ensimmäisessä pienemmässä suorakulmiossa lukee 'i32', ja toisessa pienemmässä suorakulmiossa lukee 'Cons' sekä pienempi versio ulomman 'Cons'-suorakulmion sisällä. 'Cons'-suorakulmiot sisältävät yhä pienempiä versioita itsestään, kunnes pienin mukavasti kokoinen suorakulmio sisältää äärettömyyssymbolin, mikä osoittaa, että toisto jatkuu ikuisesti.

Kuva 15-1: Ääretön List, joka koostuu äärettömistä Cons-varianteista

Rekursiivisen tyypin saaminen tunnetulla koolla

Koska Rust ei pysty selvittämään, kuinka paljon tilaa rekursiivisesti määritellyille tyypeille varataan, kääntäjä antaa virheen ja tämän hyödyllisen ehdotuksen:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

Tässä ehdotuksessa epäsuora viittaus tarkoittaa, että arvon sijaan meidän pitäisi muuttaa tietorakennetta siten, että arvo tallennetaan epäsuorasti osoittamalla arvoon sen sijaan, että tallennettaisiin arvo suoraan.

Koska Box<T> on osoitin, Rust tietää aina, kuinka paljon tilaa Box<T> tarvitsee: osoittimen koko ei muutu sen mukaan, kuinka paljon dataa se osoittaa. Tämä tarkoittaa, että voimme laittaa Box<T>:n Cons-variantin sisään toisen List-arvon sijaan. Box<T> osoittaa seuraavaan List-arvoon, joka on keolla Cons-variantin sisällä olevan arvon sijaan. Käsitteellisesti meillä on edelleen lista, joka on luotu listoista, jotka sisältävät muita listoja, mutta tämä toteutus on nyt enemmän kuin alkioiden asettaminen vierekkäin toistensa sisään sijaan.

Voimme muuttaa listauksen 15-2 List-enumin määrittelyn ja listauksen 15-3 List:in käytön listauksen 15-5 koodiksi, joka kääntyy.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-05/src/main.rs}}
}
Listing 15-5: List-määrittely, joka käyttää Box<T>:tä tunnetun koon saamiseksi

Cons-variantti tarvitsee i32:n koon plus tilan boxin osoitindatan tallentamiseen. Nil-variantti ei tallenna arvoja, joten se tarvitsee pinosta vähemmän tilaa kuin Cons-variantti. Nyt tiedämme, että mikä tahansa List-arvo vie i32:n koon plus boxin osoitindatan koon. Käyttämällä boxia olemme katkaisseet äärettömän rekursiivisen ketjun, joten kääntäjä pystyy selvittämään tarvitsemansa koon List-arvon tallentamiseen. Kuva 15-2 näyttää, miltä Cons-variantti näyttää nyt.

Suorakulmio, jossa lukee 'Cons' ja joka on jaettu kahteen pienempään suorakulmioon. Ensimmäisessä pienemmässä suorakulmiossa lukee 'i32', ja toisessa pienemmässä suorakulmiossa lukee 'Box' sekä yksi sisäinen suorakulmio, jossa lukee 'usize' ja joka edustaa boxin osoittimen äärellistä kokoa.

Kuva 15-2: List, joka ei ole äärettömän kokoinen, koska Cons sisältää Box:in

Boxit tarjoavat vain epäsuoran viittauksen ja keolle allokoinnin; niillä ei ole muita erityisominaisuuksia, kuten muilla älykkäillä osoittimilla, joihin tutustumme. Niillä ei myöskään ole niiden erityisominaisuuksien aiheuttamaa suorituskykyhaittaa, joten ne voivat olla hyödyllisiä cons-listan kaltaisissa tapauksissa, joissa epäsuora viittaus on ainoa tarvittava ominaisuus. Katsomme lisää boxien käyttötapauksia luvussa 18.

Box<T>-tyyppi on älykäs osoitin, koska se toteuttaa Deref-traitin, jonka ansiosta Box<T>-arvoja voidaan käsitellä viitteiden tavoin. Kun Box<T>-arvo poistuu näkyvyysalueeltaan, myös boxin osoittama keon data siivotaan Drop-traitin toteutuksen ansiosta. Nämä kaksi traitia ovat vielä tärkeämpiä muille älykkäille osoitintyypeille, joita käsittelemme loppuosassa tätä lukua. Tutustutaan seuraavaksi näihin kahteen traitiin tarkemmin.

Älykkäiden osoittimien käsitteleminen tavallisina viitteinä

Älykkäiden osoittimien käsittely tavallisten viitteiden tavoin

Deref-traitin toteuttaminen antaa sinun mukauttaa dereferenssioperaattorin * käyttäytymistä (älä sekoita sitä kertolasku- tai globaalioperaattoriin). Kun toteutat Deref-traitin siten, että älykästä osoitinta voidaan käsitellä tavallisen viitteen tavoin, voit kirjoittaa koodia, joka toimii viitteillä, ja käyttää sitä koodia myös älykkäiden osoittimien kanssa.

Katsotaan ensin, miten dereferenssioperaattori toimii tavallisten viitteiden kanssa. Sitten yritämme määritellä oman tyypin, joka käyttäytyy kuten Box<T>, ja näemme, miksi dereferenssioperaattori ei toimi viitteen tavoin juuri määrittämässämme tyypissä. Tutkimme, miten Deref-traitin toteuttaminen mahdollistaa älykkäiden osoittimien toiminnan viitteiden tapaan. Lopuksi katsomme Rustin deref-muunnosominaisuutta ja sitä, miten se antaa meidän työskennellä joko viitteiden tai älykkäiden osoittimien kanssa.

Viitteen seuraaminen arvoon

Tavallinen viite on eräs osoitintyyppi, ja yksi tapa ajatella osoitinta on nuolena jossain muualla tallennettuun arvoon. Listauksessa 15-6 luomme viitteen i32-arvoon ja käytämme dereferenssioperaattoria seurataksemme viitettä arvoon.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-06/src/main.rs}}
}
Listing 15-6: Dereferenssioperaattorin käyttö viitteen seuraamiseen i32-arvoon

Muuttuja x sisältää i32-arvon 5. Asetamme y:n viittaamaan x:ään. Voimme varmistaa, että x on yhtä suuri kuin 5. Jos haluamme kuitenkin tehdä väittämän y:n arvosta, meidän on käytettävä *y:tä seurataksemme viitettä arvoon, johon se osoittaa (siksi dereferenssi), jotta kääntäjä voi vertailla varsinaista arvoa. Kun olemme dereferoineet y:n, pääsemme käsiksi kokonaislukuun, johon y osoittaa, ja voimme vertailla sitä lukuun 5.

Jos yrittäisimme kirjoittaa assert_eq!(5, y); sen sijaan, saisimme tämän käännösvirheen:

{{#include ../listings/ch15-smart-pointers/output-only-01-comparing-to-reference/output.txt}}

Luvun ja viittauksen lukuun vertaileminen ei ole sallittua, koska ne ovat eri tyyppejä. Meidän on käytettävä dereferenssioperaattoria seurataksemme viitettä arvoon, johon se osoittaa.

Box<T>:n käyttäminen viitteen tavoin

Voimme kirjoittaa listauksen 15-6 koodin uudelleen käyttämään Box<T>:tä viitteen sijaan; listauksessa 15-7 Box<T>:ään käytetty dereferenssioperaattori toimii samalla tavalla kuin listauksessa 15-6 viitteeseen käytetty dereferenssioperaattori.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-07/src/main.rs}}
}
Listing 15-7: Dereferenssioperaattorin käyttö Box<i32>:llä

Listauksen 15-7 ja listauksen 15-6 pääero on, että tässä asetamme y:n olevan Box<T>-instanssi, joka osoittaa x:n kopioidun arvon, eikä viite, joka osoittaa x:n arvoon. Viimeisessä väittämässä voimme käyttää dereferenssioperaattoria seurataksemme boxin osoitinta samalla tavalla kuin silloin, kun y oli viite. Seuraavaksi tutkimme, mikä Box<T>:ssä on erityistä ja mahdollistaa dereferenssioperaattorin käytön määrittelemällä oman box-tyypin.

Oman älykkään osoittimen määrittely

Rakennetaan käärepintatyyppi, joka on samanlainen kuin standardikirjaston tarjoama Box<T>-tyyppi, jotta näemme, miten älykkäät osoittimet käyttäytyvät oletuksena eri tavalla kuin viitteet. Sitten katsomme, miten lisätään kyky käyttää dereferenssioperaattoria.

Huom: Rakennettavassa MyBox<T>-tyypissä on yksi suuri ero oikeaan Box<T>:ään verrattuna: versiomme ei tallenna dataa keolle. Keskitymme tässä esimerkissä Deref-traitiin, joten datan varsinaisen tallennuspaikan merkitys on vähemmän tärkeä kuin osoitinmaiselle käyttäytymiselle.

Box<T>-tyyppi on lopulta määritelty yhden elementin tuple-structina, joten listauksessa 15-8 määritellään MyBox<T>-tyyppi samalla tavalla. Määrittelemme myös new-funktion vastaamaan Box<T>:ään määriteltyä new-funktiota.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-08/src/main.rs:here}}
}
Listing 15-8: MyBox<T>-tyypin määrittely

Määrittelemme structin nimeltä MyBox ja ilmoitamme geneerisen parametrin T, koska haluamme tyypin voivan sisältää minkä tahansa tyyppisiä arvoja. MyBox-tyyppi on tuple-struct, jossa on yksi T-tyyppinen elementti. MyBox::new-funktio ottaa yhden T-tyyppisen parametrin ja palauttaa MyBox-instanssin, joka sisältää annetun arvon.

Yritetään lisätä listauksen 15-7 main-funktio listaukseen 15-8 ja muuttaa se käyttämään määrittämäämme MyBox<T>-tyyppiä Box<T>:n sijaan. Listauksen 15-9 koodi ei käänny, koska Rust ei tiedä, miten MyBox:ia dereferoidaan.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-09/src/main.rs:here}}
Listing 15-9: Yritys käyttää MyBox<T>:tä samalla tavalla kuin viitteitä ja Box<T>:tä

Tässä on tuloksena oleva käännösvirhe:

{{#include ../listings/ch15-smart-pointers/listing-15-09/output.txt}}

MyBox<T>-tyyppiä ei voi dereferoida, koska emme ole toteuttaneet sitä kykyä tyypille. Jotta dereferointi *-operaattorilla onnistuisi, toteutamme Deref-traitin.

Deref-traitin toteuttaminen

Kuten käsiteltiin ”Traitin toteuttaminen tyypille” -osiossa luvussa 10, traitin toteuttamiseksi meidän on annettava toteutukset traitin vaatimille metodeille. Standardikirjaston tarjoama Deref-trait vaatii meitä toteuttamaan yhden metodin nimeltä deref, joka lainaa self:n ja palauttaa viitteen sisäiseen dataan. Listauksessa 15-10 on Deref-toteutus, joka lisätään MyBox<T>:n määrittelyyn.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-10/src/main.rs:here}}
}
Listing 15-10: Deref-traitin toteuttaminen MyBox<T>:lle

type Target = T; -syntaksi määrittelee assosioituneen tyypin, jota Deref-trait käyttää. Assosioituneet tyypit ovat hieman erilainen tapa ilmoittaa geneerinen parametri, mutta sinun ei tarvitse huolehtia niistä nyt; käsittelemme niitä tarkemmin luvussa 20.

Täytämme deref-metodin rungon arvolla &self.0, jotta deref palauttaa viitteen arvoon, johon haluamme päästä *-operaattorilla; muista luvun 5 osiosta ”Eri tyyppien luominen tuple-structien avulla”, että .0 pääsee tuple-structin ensimmäiseen arvoon. Listauksen 15-9 main-funktio, joka kutsuu *:ää MyBox<T>-arvolla, kääntyy nyt, ja väittämät menevät läpi!

Ilman Deref-traitia kääntäjä voi dereferoida vain &-viitteitä. deref-metodi antaa kääntäjälle kyvyn ottaa mikä tahansa Deref-traitin toteuttava tyyppi ja kutsua deref-metodia saadakseen viitteen, jonka dereferointi se osaa.

Kun kirjoitimme *y listauksessa 15-9, Rust suoritti kulissien takana tämän koodin:

*(y.deref())

Rust korvaa *-operaattorin kutsulla deref-metodiin ja sitten tavallisella dereferoinnilla, jotta meidän ei tarvitse miettiä, pitääkö deref-metodia kutsua. Tämä Rustin ominaisuus antaa meidän kirjoittaa koodia, joka toimii identtisesti riippumatta siitä, onko meillä tavallinen viite vai Deref-traitin toteuttava tyyppi.

Syy siihen, miksi deref-metodi palauttaa viitteen arvoon ja miksi tavallinen dereferointi sulkujen ulkopuolella lausekkeessa *(y.deref()) on silti tarpeen, liittyy omistajuusjärjestelmään. Jos deref-metodi palauttaisi arvon suoraan viittauksen sijaan, arvo siirtyisi pois self:stä. Emme halua ottaa omistajuutta MyBox<T>:n sisäisestä arvosta tässä tapauksessa eikä useimmissa tapauksissa, joissa käytämme dereferenssioperaattoria.

Huomaa, että *-operaattori korvataan kutsulla deref-metodiin ja sitten kutsulla *-operaattoriin vain kerran joka kerta, kun käytämme *:ää koodissamme. Koska *-operaattorin korvaaminen ei toistu loputtomasti, päädymme lopulta i32-tyyppiseen dataan, joka vastaa listauksen 15-9 assert_eq!-lausekkeen 5:ttä.

Deref-muunnoksen käyttö funktioissa ja metodeissa

Deref-muunnos muuntaa viitteen tyypille, joka toteuttaa Deref-traitin, viitteeksi toiseen tyyppiin. Esimerkiksi deref-muunnos voi muuntaa &String:n &str:ksi, koska String toteuttaa Deref-traitin niin, että se palauttaa &str:n. Deref-muunnos on Rustin tarjoama kätevyys funktioiden ja metodien argumenteille, ja se toimii vain tyypeille, jotka toteuttavat Deref-traitin. Se tapahtuu automaattisesti, kun välitämme viitteen tietyn tyypin arvoon funktiolle tai metodille, joka ei vastaa funktion tai metodin määrittelyssä olevaa parametrietyyppiä. Sarja kutsuja deref-metodiin muuntaa antamamme tyypin parametrin vaatimaan tyyppiin.

Deref-muunnos lisättiin Rustiin, jotta funktio- ja metodikutsuja kirjoittavat ohjelmoijat eivät joutuisi lisäämään yhtä monta eksplisiittistä viitettä ja dereferenssiä &- ja *-operaattoreilla. Deref-muunnos antaa myös kirjoittaa enemmän koodia, joka toimii joko viitteillä tai älykkäillä osoittimilla.

Nähdäksemme deref-muunnoksen toiminnassa, käytetään listauksessa 15-8 määriteltyä MyBox<T>-tyyppiä sekä listauksessa 15-10 lisättyä Deref-toteutusta. Listauksessa 15-11 on funktion määrittely, jolla on merkkijonoviipaleparametri.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-11/src/main.rs:here}}
}
Listing 15-11: hello-funktio, jonka parametrilla name on tyyppi &str

Voimme kutsua hello-funktiota merkkijonoviipaleargumentilla, esimerkiksi hello("Rust");. Deref-muunnos mahdollistaa hello-funktion kutsumisen viitteellä MyBox<String>-tyyppiseen arvoon, kuten listauksessa 15-12 näytetään.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-12/src/main.rs:here}}
}
Listing 15-12: hello-funktion kutsuminen viitteellä MyBox<String>-arvoon, mikä toimii deref-muunnoksen ansiosta

Tässä kutsumme hello-funktiota argumentilla &m, joka on viite MyBox<String>-arvoon. Koska toteutimme Deref-traitin MyBox<T>:lle listauksessa 15-10, Rust voi muuntaa &MyBox<String>:n &String:ksi kutsumalla deref:iä. Standardikirjasto tarjoaa Deref-toteutuksen String:lle, joka palauttaa merkkijonoviipaleen, ja tämä on Deref:in API-dokumentaatiossa. Rust kutsuu deref:iä uudelleen muuntaakseen &String:n &str:ksi, mikä vastaa hello-funktion määrittelyä.

Jos Rust ei toteuttaisi deref-muunnosta, meidän pitäisi kirjoittaa listauksen 15-13 koodi listauksen 15-12 koodin sijaan kutsuaksemme hello:a &MyBox<String>-tyyppisellä arvolla.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-13/src/main.rs:here}}
}
Listing 15-13: Koodi, jonka meidän pitäisi kirjoittaa, jos Rustissa ei olisi deref-muunnosta

(*m) dereferoi MyBox<String>:n String:ksi. Sitten & ja [..] ottavat String:stä merkkijonoviipaleen, joka on koko merkkijono, vastatakseen hello:n signatuuria. Tämä koodi ilman deref-muunnoksia on vaikeampi lukea, kirjoittaa ja ymmärtää kaikkine näine symboleineen. Deref-muunnos antaa Rustin käsitellä nämä muunnokset automaattisesti puolestamme.

Kun Deref-trait on määritelty mukana oleville tyypeille, Rust analysoi tyypit ja käyttää Deref::deref:iä niin monta kertaa kuin tarvitaan saadakseen viitteen vastaamaan parametrin tyyppiä. Deref::deref:in lisäysten määrä ratkaistaan käännösaikana, joten deref-muunnoksen hyödyntämisestä ei ole suorituskykyhaittaa ajonaikana!

Deref-muunnoksen käsittely muuttuvien viitteiden kanssa

Samalla tavalla kuin käytät Deref-traitia korvaamaan *-operaattorin muuttumattomilla viitteillä, voit käyttää DerefMut-traitia korvaamaan *-operaattorin muuttuvilla viitteillä.

Rust tekee deref-muunnoksen, kun se löytää tyypit ja trait-toteutukset kolmessa tapauksessa:

  1. &T:stä &U:ksi, kun T: Deref<Target=U>
  2. &mut T:stä &mut U:ksi, kun T: DerefMut<Target=U>
  3. &mut T:stä &U:ksi, kun T: Deref<Target=U>

Kaksi ensimmäistä tapausta ovat samanlaiset paitsi että toinen toteuttaa mutabiliteetin. Ensimmäinen tapaus sanoo, että jos sinulla on &T ja T toteuttaa Deref:in johonkin tyyppiin U, voit saada &U:n läpinäkyvästi. Toinen tapaus sanoo, että sama deref-muunnos tapahtuu muuttuville viitteille.

Kolmas tapaus on hankalampi: Rust muuntaa myös muuttuvan viitteen muuttumattomaksi. Mutta päinvastainen ei ole mahdollista: muuttumattomia viitteitä ei koskaan muunneta muuttuviksi viitteiksi. Lainaussääntöjen vuoksi, jos sinulla on muuttuva viite, sen täytyy olla ainoa viite kyseiseen dataan (muuten ohjelma ei kääntyisi). Yhden muuttuvan viitteen muuntaminen yhteen muuttumattomaan viitteeseen ei koskaan riko lainaussääntöjä. Muuttumattoman viitteen muuntaminen muuttuvaksi viitteeksi vaatisi, että alkuperäinen muuttumaton viite on ainoa muuttumaton viite kyseiseen dataan, mutta lainaussäännöt eivät takaa sitä. Siksi Rust ei voi olettaa, että muuttumattoman viitteen muuntaminen muuttuvaksi viitteeksi on mahdollista.

Koodin suorittaminen siivouksessa Drop-traitin avulla

Koodin suorittaminen siivouksen yhteydessä Drop-traitin avulla

Toinen älykkäiden osoittimien mallin kannalta tärkeä trait on Drop, jonka avulla voit mukauttaa, mitä tapahtuu, kun arvo on poistumassa näkyvyysalueeltaan. Voit toteuttaa Drop-traitin mille tahansa tyypille, ja sitä voidaan käyttää resurssien, kuten tiedostojen tai verkkoyhteyksien, vapauttamiseen.

Esittelemme Drop-traitin älykkäiden osoittimien yhteydessä, koska Drop-traitin toiminnallisuutta käytetään lähes aina älykästä osoitinta toteutettaessa. Esimerkiksi kun Box<T> vapautetaan, se vapauttaa keon tilan, johon box osoittaa.

Joissakin kielissä ja joillekin tyypeille ohjelmoijan on kutsuttava koodia muistin tai resurssien vapauttamiseksi aina, kun hän lopettaa kyseisen tyypin instanssin käytön. Esimerkkejä ovat tiedostokahvat, soketit ja lukot. Jos ohjelmoija unohtaa tämän, järjestelmä voi ylikuormittua ja kaatua. Rustissa voit määrittää, että tietty koodi suoritetaan aina, kun arvo poistuu näkyvyysalueeltaan, ja kääntäjä lisää tämän koodin automaattisesti. Näin sinun ei tarvitse huolehtia siivouskoodin sijoittamisesta kaikkialle ohjelmaan, missä tietyn tyypin instanssi on käytetty loppuun — et silti vuoda resursseja!

Määrität koodin, joka suoritetaan arvon poistuessa näkyvyysalueeltaan, toteuttamalla Drop-traitin. Drop-trait vaatii yhden metodin nimeltä drop, joka ottaa muuttuvan viitteen self:iin. Nähdäksemme, milloin Rust kutsuu drop:ia, toteutetaan drop toistaiseksi println!-lauseilla.

Listauksessa 15-14 on CustomSmartPointer-struct, jonka ainoa mukautettu toiminnallisuus on tulostaa Dropping CustomSmartPointer!, kun instanssi poistuu näkyvyysalueeltaan, jotta näemme, milloin Rust suorittaa drop-metodin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-14/src/main.rs}}
}
Listing 15-14: CustomSmartPointer-struct, joka toteuttaa Drop-traitin ja johon voisimme sijoittaa siivouskoodimme

Drop-trait sisältyy preludiin, joten meidän ei tarvitse tuoda sitä näkyvyysalueelle. Toteutamme Drop-traitin CustomSmartPointer:lle ja annamme drop-metodille toteutuksen, joka kutsuu println!:ää. drop-metodin runkoon sijoitettaisiin logiikka, jonka haluaisit suorittaa, kun tyypin instanssi poistuu näkyvyysalueeltaan. Tulostamme tässä tekstiä havainnollistamaan visuaalisesti, milloin Rust kutsuu drop:ia.

main-funktiossa luomme kaksi CustomSmartPointer-instanssia ja tulostamme sitten CustomSmartPointers created. main-funktion lopussa CustomSmartPointer-instanssimme poistuvat näkyvyysalueeltaan, ja Rust kutsuu drop-metodiin sijoittamaamme koodia tulostaen viimeisen viestimme. Huomaa, ettemme tarvinneet kutsua drop-metodia erikseen.

Kun ajamme tämän ohjelman, näemme seuraavan tulosteen:

{{#include ../listings/ch15-smart-pointers/listing-15-14/output.txt}}

Rust kutsui drop:ia automaattisesti, kun instanssimme poistuivat näkyvyysalueeltaan, ja suoritti määrittämämme koodin. Muuttujat vapautetaan käänteisessä luontijärjestyksessä, joten d vapautettiin ennen c:tä. Tämän esimerkin tarkoitus on antaa visuaalinen opas siihen, miten drop-metodi toimii; yleensä määrittäisit tyypillesi tarvittavan siivouskoodin tulostusviestin sijaan.

Valitettavasti automaattisen drop-toiminnallisuuden poistaminen käytöstä ei ole suoraviivaista. drop:in poistaminen käytöstä ei yleensä ole tarpeen; Drop-traitin koko pointti on, että siitä huolehditaan automaattisesti. Joskus kuitenkin saatat haluta siivota arvon aikaisin. Esimerkki on lukkoja hallitsevien älykkäiden osoittimien käyttö: saatat haluta pakottaa drop-metodin, joka vapauttaa lukon, jotta muu koodi samassa näkyvyysalueessa voi hankkia lukon. Rust ei anna sinun kutsua Drop-traitin drop-metodia käsin; sen sijaan sinun on kutsuttava standardikirjaston tarjoamaa std::mem::drop-funktiota, jos haluat pakottaa arvon vapauttamisen ennen näkyvyysalueen loppua.

Yritys kutsua Drop-traitin drop-metodia käsin muokkaamalla listauksen 15-14 main-funktiota ei toimi, kuten listauksessa 15-15 näytetään.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-15/src/main.rs:here}}
Listing 15-15: Yritys kutsua Drop-traitin drop-metodia käsin aikaiseen siivoukseen

Kun yritämme kääntää tämän koodin, saamme tämän virheen:

{{#include ../listings/ch15-smart-pointers/listing-15-15/output.txt}}

Tämä virheilmoitus kertoo, ettemme saa kutsua drop:ia eksplisiittisesti. Virheilmoituksessa käytetään termiä destructor (destruktori), joka on yleinen ohjelmointitermi instanssin siivoavalle funktiolle. Destruktori vastaa konstruktoria, joka luo instanssin. Rustin drop-funktio on yksi tietty destruktori.

Rust ei anna meidän kutsua drop:ia eksplisiittisesti, koska Rust kutsuisi silti drop:ia automaattisesti arvolle main-funktion lopussa. Tämä aiheuttaisi double free -virheen, koska Rust yrittäisi siivota saman arvon kahdesti.

Emme voi poistaa käytöstä automaattista drop-lisäystä arvon poistuessa näkyvyysalueeltaan, emmekä voi kutsua drop-metodia eksplisiittisesti. Jos meidän täytyy pakottaa arvon siivous aikaisin, käytämme std::mem::drop-funktiota.

std::mem::drop-funktio eroaa Drop-traitin drop-metodista. Kutsumme sitä välittämällä argumenttina arvon, jonka haluamme pakottaa vapautettavaksi. Funktio on preludissa, joten voimme muokata listauksen 15-15 main-funktiota kutsumaan drop-funktiota, kuten listauksessa 15-16.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-16/src/main.rs:here}}
}
Listing 15-16: std::mem::drop-funktion kutsuminen arvon eksplisiittiseen vapauttamiseen ennen näkyvyysalueen loppua

Tämän koodin ajaminen tulostaa seuraavaa:

{{#include ../listings/ch15-smart-pointers/listing-15-16/output.txt}}

Teksti Dropping CustomSmartPointer with data `some data`! tulostuu CustomSmartPointer created- ja CustomSmartPointer dropped before the end of main -tekstien väliin, mikä osoittaa, että drop-metodin koodi kutsutaan vapauttamaan c tuossa vaiheessa.

Voit käyttää Drop-traitin toteutuksessa määriteltyä koodia monin tavoin tehdäksesi siivouksesta kätevää ja turvallista: voisit esimerkiksi luoda oman muistinhallintajärjestelmän! Drop-traitin ja Rustin omistajuusjärjestelmän ansiosta sinun ei tarvitse muistaa siivota, koska Rust tekee sen automaattisesti.

Sinun ei myöskään tarvitse huolehtia ongelmista, jotka johtuvat vahingossa käytössä olevien arvojen siivoamisesta: omistajuusjärjestelmä, joka varmistaa viitteiden kelpoisuuden, varmistaa myös, että drop kutsutaan vain kerran, kun arvoa ei enää käytetä.

Nyt kun olemme tarkastelleet Box<T>:tä ja joitakin älykkäiden osoittimien ominaisuuksia, katsomme muutamia muita standardikirjaston määrittelemiä älykkäitä osoittimia.

Rc<T>, viitelaskennallinen älykäs osoitin

Rc<T>, viittauslaskennan älykäs osoitin

Useimmissa tapauksissa omistajuus on selvä: tiedät tarkalleen, mikä muuttuja omistaa tietyn arvon. On kuitenkin tilanteita, joissa yhdellä arvolla voi olla useita omistajia. Graafitietorakenteissa esimerkiksi useat reunat voivat osoittaa samaan solmuun, ja kyseinen solmu on käsitteellisesti kaikkien siihen osoittavien reunojen omistama. Solmua ei pitäisi siivota, ellei siihen osoita yhtään reunaa ja sillä siten ei ole omistajia.

Usean omistajuuden täytyy ottaa käyttöön eksplisiittisesti Rustin tyypillä Rc<T>, joka on lyhenne sanasta reference counting (viittauslaskenta). Rc<T>-tyyppi seuraa viittausten määrää arvoon ja päättelee, onko arvo vielä käytössä. Jos arvoon ei ole yhtään viittausta, arvo voidaan siivota ilman, että viittaukset muuttuvat virheellisiksi.

Kuvittele Rc<T> perhehuoneen televisioksi. Kun yksi henkilö tulee katsomaan televisiota, hän kytkee sen päälle. Muut voivat tulla huoneeseen ja katsoa televisiota. Kun viimeinen henkilö poistuu huoneesta, hän sammuttaa television, koska sitä ei enää käytetä. Jos joku sammuttaisi television, vaikka muut vielä katsoisivat sitä, jäljellä olevat katsojat protestoisivat!

Käytämme Rc<T>-tyyppiä, kun haluamme varata dataa keolle ohjelman useille osille luettavaksi emmekä voi käännösaikana tietää, mikä osa lopettaa datan käytön viimeisenä. Jos tietäisimme, mikä osa lopettaa viimeisenä, voisimme tehdä siitä datan omistajan, ja käännösaikana voimaan tulevat tavalliset omistajuussäännöt riittäisivät.

Huomaa, että Rc<T> on tarkoitettu vain yksisäikeisiin skenaarioihin. Kun käsittelemme rinnakkaisuutta luvussa 16, käymme läpi, miten viittauslaskentaa tehdään monisäikeisissä ohjelmissa.

Datan jakaminen

Palataan cons-listan esimerkkiin listauksessa 15-5. Muistathan, että määrittelimme sen käyttäen Box<T>:tä. Tällä kertaa luomme kaksi listaa, jotka molemmat jakavat omistajuuden kolmannesta listasta. Käsitteellisesti tämä näyttää samankaltaiselta kuin kuvassa 15-3.

A linked list with the label 'a' pointing to three elements. The first element contains the integer 5 and points to the second element. Th
e second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the l
ist; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element o
f list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a' so th
at the tails of lists 'b' and 'c' are both list 'a'.

Kuva 15-3: Kaksi listaa, b ja c, jakavat omistajuuden kolmannesta listasta, a

Luomme listan a, joka sisältää 5:n ja sitten 10:n. Sitten teemme kaksi listaa lisää: b, joka alkaa 3:lla, ja c, joka alkaa 4:llä. Sekä b- että c-listat jatkavat ensimmäiseen a-listaan, joka sisältää 5:n ja 10:n. Toisin sanoen molemmat listat jakavat ensimmäisen listan, joka sisältää 5:n ja 10:n.

Yritys toteuttaa tämä skenaario List-määrittelyllämme Box<T>:llä ei toimi, kuten listauksessa 15-17 näytetään.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-17/src/main.rs}}
Listing 15-17: Osoitus siihen, ettei meillä ole lupaa käyttää kahta Box<T>-listaa, jotka yrittävät jakaa omistajuuden kolmannesta listasta

Kun käännämme tämän koodin, saamme tämän virheen:

{{#include ../listings/ch15-smart-pointers/listing-15-17/output.txt}}

Cons-variantit omistavat pitämänsä datan, joten kun luomme b-listan, a siirretään b:hen ja b omistaa a:n. Sitten kun yritämme käyttää a:ta uudelleen luodessamme c:n, se ei ole sallittua, koska a on jo siirretty.

Voisimme muuttaa Cons-määrittelyä pitämään viittauksia sen sijaan, mutta silloin meidän täytyisi määrittää elinaikaparametrit. Määrittämällä elinaikaparametrit ilmoittaisimme, että jokainen listan elementti elää vähintään yhtä kauan kuin koko lista. Näin on listauksen 15-17 elementeissä ja listoissa, mutta ei kaikissa skenaarioissa.

Sen sijaan muutamme List-määrittelyämme käyttämään Rc<T>:tä Box<T>:n sijaan, kuten listauksessa 15-18. Jokainen Cons-variantti pitää nyt arvon ja Rc<T>:n, joka osoittaa List:iin. Kun luomme b:n, sen sijaan että ottaisimme a:n omistajuuden, kloonaamme Rc<List>:n, jota a pitää, mikä kasvattaa viittausten määrää yhdestä kahteen ja antaa a:n ja b:n jakaa omistajuuden kyseisen Rc<List>:n datassa. Kloonaamme myös a:n luodessamme c:n, mikä kasvattaa viittausten määrää kahdesta kolmeen. Joka kerta kun kutsumme Rc::clone:a, Rc<List>:n sisällä olevan datan viittauslaskuri kasvaa, eikä dataa siivota, ellei viittauksia ole nolla.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-18/src/main.rs}}
}
Listing 15-18: List-määrittely, joka käyttää Rc<T>:tä

Meidän täytyy lisätä use-lause tuomaan Rc<T> näkyvyysalueelle, koska se ei ole preludissa. main-funktiossa luomme listan, joka pitää 5:tä ja 10:tä, ja tallennamme sen uuteen Rc<List>:iin muuttujassa a. Sitten kun luomme b:n ja c:n, kutsumme Rc::clone-funktiota ja välitämme argumenttina viittauksen a:n Rc<List>:iin.

Olisimme voineet kutsua a.clone():a Rc::clone(&a):n sijaan, mutta Rustin käytäntö on käyttää tässä tapauksessa Rc::clone:a. Rc::clone:n toteutus ei tee syväkopiota kaikesta datasta kuten useimpien tyyppien clone-toteutukset. Rc::clone-kutsu kasvattaa vain viittauslaskuria, mikä ei vie paljon aikaa. Datan syväkopiot voivat viedä paljon aikaa. Käyttämällä Rc::clone:a viittauslaskentaan voimme visuaalisesti erottaa syväkopioivien kloonausten ja viittauslaskuria kasvattavien kloonausten välillä. Kun etsimme suorituskykyongelmia koodista, meidän tarvitsee huomioida vain syväkopioivat kloonaukset ja voimme jättää huomiotta Rc::clone-kutsut.

Kloonaus viittauslaskurin kasvattamiseksi

Muutetaan listauksen 15-18 työesimerkkiä niin, että näemme viittauslaskurin muuttuvan luodessamme ja pudottaessamme viittauksia a:n Rc<List>:iin.

Listauksessa 15-19 muutamme main:ia niin, että siinä on sisäinen näkyvyysalue listan c ympärillä; sitten voimme nähdä, miten viittauslaskuri muuttuu, kun c poistuu näkyvyysalueelta.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-19/src/main.rs:here}}
}
Listing 15-19: Viittauslaskurin tulostaminen

Jokaisessa ohjelman kohdassa, jossa viittauslaskuri muuttuu, tulostamme viittauslaskurin kutsumalla Rc::strong_count-funktiota. Funktio on nimetty strong_count:ksi count:n sijaan, koska Rc<T>-tyypillä on myös weak_count; näemme, mihin weak_count:ia käytetään kohdassa „Viittauskiertojen estäminen Weak<T>:n avulla”.

Tämä koodi tulostaa seuraavaa:

{{#include ../listings/ch15-smart-pointers/listing-15-19/output.txt}}

Näemme, että a:n Rc<List>:llä on aluksi viittauslaskuri 1; sitten joka kerta kun kutsumme clone:a, laskuri kasvaa yhdellä. Kun c poistuu näkyvyysalueelta, laskuri pienenee yhdellä. Meidän ei tarvitse kutsua funktiota viittauslaskurin pienentämiseksi kuten meidän täytyy kutsua Rc::clone:a viittauslaskurin kasvattamiseksi: Drop-traitin toteutus pienentää viittauslaskuria automaattisesti, kun Rc<T>-arvo poistuu näkyvyysalueelta.

Mitä emme näe tässä esimerkissä, on se, että kun b ja sitten a poistuvat näkyvyysalueelta main:in lopussa, laskuri on 0 ja Rc<List> siivotaan kokonaan. Rc<T>:n avulla yhdellä arvolla voi olla useita omistajia, ja laskuri varmistaa, että arvo pysyy voimassa niin kauan kuin yksikään omistajista on olemassa.

Muuttumattomien viittausten kautta Rc<T> antaa sinun jakaa dataa ohjelman useiden osien välillä vain lukemista varten. Jos Rc<T> sallisi myös useita muuttuvia viittauksia, saatat rikkoa yhden luvussa 4 käsitellyistä lainausperiaatteista: useat muuttuvat lainaukset samaan paikkaan voivat aiheuttaa datakilpailuja ja epäjohdonmukaisuuksia. Datan muuttaminen on kuitenkin hyvin hyödyllistä! Seuraavassa osiossa käsittelemme sisäisen muuttuvuuden mallia ja RefCell<T>-tyyppiä, jota voit käyttää yhdessä Rc<T>:n kanssa tämän muuttumattomuusrajoituksen kanssa työskentelyyn.

RefCell<T> ja sisäisen muuttuvuuden malli

RefCell<T> ja sisäisen muuttuvuuden malli

Sisäinen muuttuvuus on Rustissa suunnittelumalli, joka sallii datan muuttamisen, vaikka dataan olisi muuttumattomia viittauksia; normaalisti lainausperiaatteet kieltävät tämän toiminnon. Datan muuttamiseksi malli käyttää unsafe-koodia tietorakenteen sisällä taivuttaakseen Rustin tavallisia sääntöjä, jotka hallitsevat muuttamista ja lainaamista. Unsafe-koodi ilmaisee kääntäjälle, että tarkistamme säännöt käsin sen sijaan, että luottaisimme kääntäjään niiden tarkistamisessa; käsittelemme unsafe-koodia tarkemmin luvussa 20.

Voimme käyttää sisäisen muuttuvuuden mallia noudattavia tyyppejä vain, kun voimme varmistaa, että lainausperiaatteita noudatetaan ajonaikana, vaikka kääntäjä ei voi sitä taata. Mukana oleva unsafe-koodi kääritään sitten turvalliseen API:in, ja ulompi tyyppi on edelleen muuttumaton.

Tutustutaan tähän käsitteeseen tarkastelemalla RefCell<T>-tyyppiä, joka noudattaa sisäisen muuttuvuuden mallia.

Lainausperiaatteiden pakottaminen ajonaikana

Toisin kuin Rc<T>, RefCell<T>-tyyppi edustaa yksittäistä omistajuutta pitämänsä datan yli. Mikä siis tekee RefCell<T>:stä erilaisen kuin esimerkiksi Box<T>? Muistathan luvussa 4 opitut lainausperiaatteet:

  • Milloin tahansa sinulla voi olla joko yksi muuttuva viittaus tai mikä tahansa määrä muuttumattomia viittauksia (mutta ei molempia).
  • Viittausten täytyy aina olla voimassa.

Viittausten ja Box<T>:n kanssa lainausperiaatteiden invariantit pakotetaan käännösaikana. RefCell<T>:n kanssa nämä invariantit pakotetaan ajonaikana. Viittausten kanssa sääntöjen rikkominen tuottaa kääntäjävirheen. RefCell<T>:n kanssa sääntöjen rikkominen saa ohjelman panikoimaan ja päättymään.

Lainausperiaatteiden tarkistamisen käännösaikana etuja ovat, että virheet havaitaan aikaisemmin kehitysprosessissa eikä ajonaikaisella suorituskyvyllä ole vaikutusta, koska kaikki analyysi tehdään etukäteen. Näistä syistä lainausperiaatteiden tarkistaminen käännösaikana on paras valinta useimmissa tapauksissa, minkä vuoksi se on Rustin oletus.

Lainausperiaatteiden tarkistamisen ajonaikana etuna on, että tietyt muistiturvalliset skenaariot sallitaan, vaikka käännösaikaiset tarkistukset olisivat kieltäneet ne. Staattinen analyysi, kuten Rustin kääntäjä, on luonnostaan konservatiivinen. Joitakin koodin ominaisuuksia on mahdotonta havaita analysoimalla koodia: tunnetuin esimerkki on pysähtymisongelma, joka on tämän kirjan ulkopuolella mutta mielenkiintoinen tutkimuskohde.

Koska osa analyysistä on mahdotonta, jos Rustin kääntäjä ei voi olla varma, että koodi noudattaa omistajuussääntöjä, se saattaa hylätä oikean ohjelman; tällä tavalla se on konservatiivinen. Jos Rust hyväksyisi virheellisen ohjelman, käyttäjät eivät voisi luottaa Rustin antamiin takuisiin. Jos Rust hylkää oikean ohjelman, ohjelmoijaa haitataan, mutta mitään katastrofaalista ei voi tapahtua. RefCell<T>-tyyppi on hyödyllinen, kun olet varma, että koodisi noudattaa lainausperiaatteita, mutta kääntäjä ei pysty ymmärtämään ja takaamaan sitä.

Samoin kuin Rc<T>, RefCell<T> on tarkoitettu vain yksisäikeisiin skenaarioihin ja antaa kääntäjävirheen, jos yrität käyttää sitä monisäikeisessä kontekstissa. Käsittelemme, miten RefCell<T>:n toiminnallisuus saadaan monisäikeisessä ohjelmassa luvussa 16.

Tässä on yhteenveto syistä valita Box<T>, Rc<T> tai RefCell<T>:

  • Rc<T> mahdollistaa saman datan usean omistajan; Box<T>:llä ja RefCell<T>:llä on yksi omistaja.
  • Box<T> sallii muuttumattomat tai muuttuvat lainaukset, jotka tarkistetaan käännösaikana; Rc<T> sallii vain muuttumattomat lainaukset, jotka tarkistetaan käännösaikana; RefCell<T> sallii muuttumattomat tai muuttuvat lainaukset, jotka tarkistetaan ajonaikana.
  • Koska RefCell<T> sallii muuttuvat lainaukset, jotka tarkistetaan ajonaikana, voit muuttaa RefCell<T>:n sisällä olevaa arvoa, vaikka RefCell<T> itse olisi muuttumaton.

Arvon muuttaminen muuttumattoman arvon sisällä on sisäisen muuttuvuuden malli. Katsotaan tilannetta, jossa sisäinen muuttuvuus on hyödyllinen, ja tarkastellaan, miten se on mahdollista.

Sisäisen muuttuvuuden käyttö

Lainausperiaatteiden seuraauksena, kun sinulla on muuttumaton arvo, et voi lainata sitä muuttuvasti. Esimerkiksi tämä koodi ei käännä:

{{#rustdoc_include ../listings/ch15-smart-pointers/no-listing-01-cant-borrow-immutable-as-mutable/src/main.rs}}

Jos yrittäisit kääntää tämän koodin, saisit seuraavan virheen:

{{#include ../listings/ch15-smart-pointers/no-listing-01-cant-borrow-immutable-as-mutable/output.txt}}

On kuitenkin tilanteita, joissa olisi hyödyllistä, että arvo muuttaisi itseään metodeissaan mutta näyttäisi muuttumattomalta muulle koodille. Arvon metodien ulkopuolella oleva koodi ei voisi muuttaa arvoa. RefCell<T>:n käyttö on yksi tapa saada sisäinen muuttuvuus, mutta RefCell<T> ei kierrä lainausperiaatteita kokonaan: kääntäjän lainaustarkistin sallii tämän sisäisen muuttuvuuden, ja lainausperiaatteet tarkistetaan ajonaikana käännösaikaan sijaan. Jos rikot säännöt, saat panic!:n kääntäjävirheen sijaan.

Käydään läpi käytännön esimerkki, jossa voimme käyttää RefCell<T>:tä muuttamaan muuttumatonta arvoa, ja katsotaan, miksi se on hyödyllistä.

Testaus mock-olioilla

Joskus testauksen aikana ohjelmoija käyttää yhtä tyyppiä toisen tilalla havaitakseen tiettyä käyttäytymistä ja varmistaakseen, että se on toteutettu oikein. Tätä paikkamerkkityyppiä kutsutaan test double:ksi. Ajattele sitä elokuvatuotannon stunt-tuplana, jossa henkilö astuu esiin ja korvaa näyttelijän tekemään erityisen hankalan kohtauksen. Test double:t korvaavat muita tyyppejä testejä ajettaessa. Mock-oliot ovat erityisiä test double -tyyppejä, jotka tallentavat testin aikana tapahtuneet asiat, jotta voit varmistaa, että oikeat toiminnot tapahtuivat.

Rustissa ei ole olioita samassa mielessä kuin muissa kielissä, eikä Rustissa ole mock-olioiden toiminnallisuutta sisäänrakennettuna standardikirjastoon kuten joissakin muissa kielissä. Voit kuitenkin ehdottomasti luoda structin, joka palvelee samoja tarkoituksia kuin mock-olio.

Tässä on skenaario, jota testaamme: luomme kirjaston, joka seuraa arvoa suhteessa enimmäisarvoon ja lähettää viestejä sen mukaan, kuinka lähellä enimmäisarvoa nykyinen arvo on. Tätä kirjastoa voitaisiin käyttää esimerkiksi seuraamaan käyttäjän API-kutsukiintiötä.

Kirjastomme tarjoaa vain toiminnallisuuden seurata, kuinka lähellä enimmäisarvoa arvo on, ja mitä viestejä pitäisi lähettää milloin. Kirjastoa käyttävien sovellusten odotetaan tarjoavan viestien lähetysmekanismin: sovellus voisi näyttää viestin käyttäjälle suoraan, lähettää sähköpostin, tekstiviestin tai tehdä jotain muuta. Kirjaston ei tarvitse tietää yksityiskohtia. Sille riittää jokin, joka toteuttaa tarjoamamme traitin nimeltä Messenger. Listauksessa 15-20 on kirjaston koodi.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-20/src/lib.rs}}
Listing 15-20: Kirjasto, joka seuraa kuinka lähellä arvo on enimmäisarvoa ja varoittaa, kun arvo on tietyillä tasoilla

Yksi tärkeä osa tässä koodissa on, että Messenger-traitilla on yksi metodi nimeltä send, joka ottaa muuttumattoman viittauksen self:iin ja viestin tekstin. Tämä trait on rajapinta, jonka mock-oliomme täytyy toteuttaa, jotta mockia voidaan käyttää samalla tavalla kuin oikeaa oliota. Toinen tärkeä osa on, että haluamme testata LimitTracker:in set_value-metodin käyttäytymistä. Voimme muuttaa value-parametrille välittämäämme arvoa, mutta set_value ei palauta mitään, josta voisimme tehdä väitteitä. Haluamme voida sanoa, että jos luomme LimitTracker:in jollakin, joka toteuttaa Messenger-traitin, ja tietyllä max-arvolla, messengerille kerrotaan lähettämään asianmukaiset viestit, kun välitämme eri lukuja value:lle.

Tarvitsemme mock-olion, joka sähköpostin tai tekstiviestin lähettämisen sijaan, kun kutsumme send:iä, vain seuraa sille kerrottuja viestejä. Voimme luoda uuden mock-olioinstanssin, luoda LimitTracker:in, joka käyttää mock-oliota, kutsua LimitTracker:in set_value-metodia ja tarkistaa sitten, että mock-oliolla on odottamamme viestit. Listauksessa 15-21 on yritys toteuttaa mock-olio juuri tätä varten, mutta lainaustarkistin ei salli sitä.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-21/src/lib.rs:here}}
Listing 15-21: Yritys toteuttaa MockMessenger, jota lainaustarkistin ei salli

Tämä testikoodi määrittelee MockMessenger-structin, jolla on sent_messages-kenttä Vec<String>-arvolla seuratakseen sille kerrottuja viestejä. Määrittelemme myös assosioituneen funktion new, jotta uusien MockMessenger-arvojen luominen tyhjällä viestilistalla on kätevää. Toteutamme sitten Messenger-traitin MockMessenger:lle, jotta voimme antaa MockMessenger:in LimitTracker:ille. send-metodin määrittelyssä otamme parametrina välitetyn viestin ja tallennamme sen MockMessenger:in sent_messages-listaan.

Testissä testaamme, mitä tapahtuu, kun LimitTracker:ille kerrotaan asettamaan value joksikin, joka on yli 75 prosenttia max-arvosta. Ensin luomme uuden MockMessenger:in, joka alkaa tyhjällä viestilistalla. Sitten luomme uuden LimitTracker:in ja annamme sille viittauksen uuteen MockMessenger:iin ja max-arvon 100. Kutsumme LimitTracker:in set_value-metodia arvolla 80, joka on yli 75 prosenttia luvusta 100. Sitten väitämme, että MockMessenger:in seuraamalla viestilistalla pitäisi nyt olla yksi viesti.

Tässä testissä on kuitenkin yksi ongelma, kuten tässä näytetään:

{{#include ../listings/ch15-smart-pointers/listing-15-21/output.txt}}

Emme voi muokata MockMessenger:ia seurataksemme viestejä, koska send-metodi ottaa muuttumattoman viittauksen self:iin. Emme myöskään voi noudattaa virheilmoituksen ehdotusta käyttää &mut self:iä sekä impl-metodissa että traitin määrittelyssä. Emme halua muuttaa Messenger-traitia pelkästään testauksen vuoksi. Sen sijaan meidän täytyy löytää tapa saada testikoodimme toimimaan oikein olemassa olevan suunnittelumme kanssa.

Tämä on tilanne, jossa sisäinen muuttuvuus voi auttaa! Tallennamme sent_messages:in RefCell<T>:n sisään, ja sitten send-metodi voi muokata sent_messages:ia tallentaakseen näkemämme viestit. Listauksessa 15-22 näytetään, miltä se näyttää.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-22/src/lib.rs:here}}
Listing 15-22: RefCell<T>:n käyttö sisäisen arvon muuttamiseen, vaikka ulompi arvo katsotaan muuttumattomaksi

sent_messages-kenttä on nyt tyyppiä RefCell<Vec<String>> Vec<String>:n sijaan. new-funktiossa luomme uuden RefCell<Vec<String>>-instanssin tyhjän vektorin ympärille.

send-metodin toteutuksessa ensimmäinen parametri on edelleen muuttumaton lainaus self:stä, mikä vastaa traitin määrittelyä. Kutsumme borrow_mut:ia self.sent_messages:in RefCell<Vec<String>>:ssa saadaksemme muuttuvan viittauksen RefCell<Vec<String>>:n sisällä olevaan arvoon, joka on vektori. Sitten voimme kutsua push:ia vektorin muuttuvalla viittauksella seurataksemme testin aikana lähetettyjä viestejä.

Viimeinen muutos, jonka meidän täytyy tehdä, on väitteessä: nähdäksemme, kuinka monta kohdetta sisäisessä vektorissa on, kutsumme borrow:ia RefCell<Vec<String>>:ssa saadaksemme muuttumattoman viittauksen vektoriin.

Nyt kun olet nähnyt, miten RefCell<T>:tä käytetään, syvennytään siihen, miten se toimii!

Lainausten seuranta ajonaikana

Kun luomme muuttumattomia ja muuttuvia viittauksia, käytämme vastaavasti &- ja &mut-syntaksia. RefCell<T>:n kanssa käytämme borrow- ja borrow_mut-metodeja, jotka kuuluvat RefCell<T>:n turvalliseen API:in. borrow-metodi palauttaa älykkään osoittimen tyypin Ref<T>, ja borrow_mut palauttaa älykkään osoittimen tyypin RefMut<T>. Molemmat tyypit toteuttavat Deref:in, joten voimme käsitellä niitä kuin tavallisia viittauksia.

RefCell<T> seuraa, kuinka monta Ref<T>- ja RefMut<T>-älykästä osoitinta on parhaillaan aktiivisia. Joka kerta kun kutsumme borrow:ia, RefCell<T> kasvattaa aktiivisten muuttumattomien lainausten määrää. Kun Ref<T>-arvo poistuu näkyvyysalueelta, muuttumattomien lainausten määrä pienenee yhdellä. Aivan kuten käännösaikaiset lainausperiaatteet, RefCell<T> sallii useita muuttumattomia lainauksia tai yhden muuttuvan lainauksen milloin tahansa.

Jos yritämme rikkoa näitä sääntöjä, RefCell<T>:n toteutus panikoi ajonaikana sen sijaan, että saisimme kääntäjävirheen kuten viittausten kanssa. Listauksessa 15-23 on muutos listauksen 15-22 send-toteutukseen. Yritämme tarkoituksella luoda kaksi aktiivista muuttuvaa lainausta samaan näkyvyysalueeseen havainnollistaaksemme, että RefCell<T> estää meitä tekemästä tätä ajonaikana.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-23/src/lib.rs:here}}
Listing 15-23: Kahden muuttuvan viittauksen luominen samaan näkyvyysalueeseen nähdäksemme, että RefCell<T> panikoi

Luomme muuttujan one_borrow borrow_mut:ista palautetulle RefMut<T>-älykkäälle osoittimelle. Sitten luomme toisen muuttuvan lainauksen samalla tavalla muuttujaan two_borrow. Tämä tekee kaksi muuttuvaa viittausta samaan näkyvyysalueeseen, mikä ei ole sallittua. Kun ajamme kirjastomme testit, listauksen 15-23 koodi kääntyy ilman virheitä, mutta testi epäonnistuu:

{{#include ../listings/ch15-smart-pointers/listing-15-23/output.txt}}

Huomaa, että koodi panikoi viestillä already borrowed: BorrowMutError. Näin RefCell<T> käsittelee lainausperiaatteiden rikkomukset ajonaikana.

Valinta lainausvirheiden havaitsemiseen ajonaikana käännösaikaan sijaan, kuten tässä teimme, tarkoittaa, että saatat löytää virheitä koodissasi myöhemmin kehitysprosessissa: mahdollisesti vasta kun koodi on otettu tuotantoon. Lisäksi koodisi kärsii pienestä ajonaikaisesta suorituskykyrangaistuksesta lainausten seurannan vuoksi ajonaikana käännösaikaan sijaan. RefCell<T>:n käyttö mahdollistaa kuitenkin mock-olion kirjoittamisen, joka voi muokata itseään seuratakseen näkemiään viestejä kontekstissa, jossa vain muuttumattomat arvot ovat sallittuja. Voit käyttää RefCell<T>:tä sen kompromissien huolimatta saadaksesi enemmän toiminnallisuutta kuin tavalliset viittaukset tarjoavat.

Muuttuvan datan usean omistajan salliminen

Yleinen tapa käyttää RefCell<T>:tä on yhdessä Rc<T>:n kanssa. Muistathan, että Rc<T> sallii usean omistajan datalle, mutta antaa vain muuttumattoman pääsyn kyseiseen dataan. Jos sinulla on Rc<T>, joka pitää RefCell<T>:tä, voit saada arvon, jolla voi olla useita omistajia ja jota voit muuttaa!

Muistathan esimerkiksi listauksen 15-18 cons-listan esimerkin, jossa käytimme Rc<T>:tä salliaksemme useiden listojen jakaa toisen listan omistajuuden. Koska Rc<T> pitää vain muuttumattomia arvoja, emme voi muuttaa mitään listojen arvoista, kun olemme luoneet ne. Lisätään RefCell<T> sen kykyä muuttaa listojen arvoja. Listauksessa 15-24 näytetään, että käyttämällä RefCell<T>:tä Cons-määrittelyssä voimme muokata kaikissa listoissa tallennettua arvoa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-24/src/main.rs}}
}
Listing 15-24: Rc<RefCell<i32>>:n käyttö muokattavan List:in luomiseen

Luomme arvon, joka on Rc<RefCell<i32>>-instanssi, ja tallennamme sen muuttujaan nimeltä value, jotta voimme käyttää sitä suoraan myöhemmin. Sitten luomme List:in a:ssa Cons-variantilla, joka pitää value:a. Meidän täytyy kloonata value, jotta sekä a että value omistavat sisäisen arvon 5 sen sijaan, että siirtäisimme omistajuuden value:sta a:han tai että a lainaisi value:sta.

Käärimme listan a Rc<T>:hen, jotta kun luomme listat b ja c, ne voivat molemmat viitata a:han, kuten teimme listauksessa 15-18.

Kun olemme luoneet listat a:ssa, b:ssä ja c:ssä, haluamme lisätä value:n arvoon 10. Teemme tämän kutsumalla borrow_mut:ia value:lla, joka käyttää automaattista dereferointiominaisuutta, josta puhuimme kohdassa „Missä on ->-operaattori?” luvussa 5, dereferoidakseen Rc<T>:n sisäiseen RefCell<T>-arvoon. borrow_mut-metodi palauttaa RefMut<T>-älykkään osoittimen, ja käytämme dereferointioperaattoria sen päällä ja muutamme sisäistä arvoa.

Kun tulostamme a:n, b:n ja c:n, näemme, että niillä kaikilla on muokattu arvo 15 arvon 5 sijaan:

{{#include ../listings/ch15-smart-pointers/listing-15-24/output.txt}}

Tämä tekniikka on melko näppärä! RefCell<T>:n avulla meillä on ulospäin muuttumaton List-arvo. Mutta voimme käyttää RefCell<T>:n metodeja, jotka tarjoavat pääsyn sen sisäiseen muuttuvuuteen, jotta voimme muokata dataamme tarvittaessa. Lainausperiaatteiden ajonaikaiset tarkistukset suojaavat meitä datakilpailuilta, ja joskus on syytä vaihtaa hieman nopeutta tämän joustavuuden vuoksi tietorakenteissamme. Huomaa, että RefCell<T> ei toimi monisäikeisessä koodissa! Mutex<T> on RefCell<T>:n säieturvallinen versio, ja käsittelemme Mutex<T>:tä luvussa 16.

Viitesyklit voivat vuotaa muistia

Viittauskiertot voivat vuotaa muistia

Rustin muistiturvallisuustakuut tekevät vaikeaksi, mutta eivät mahdottomaksi, luoda vahingossa muistia, jota ei koskaan siivota (tunnetaan nimellä memory leak, muistivuoto). Muistivuotojen täydellinen estäminen ei ole yksi Rustin takuista, mikä tarkoittaa, että muistivuodot ovat muistiturvallisia Rustissa. Näemme, että Rust sallii muistivuodot käyttämällä Rc<T>:tä ja RefCell<T>:tä: on mahdollista luoda viittauksia, joissa kohteet viittaavat toisiinsa kiertona. Tämä luo muistivuotoja, koska kunkin kierron kohteen viittauslaskuri ei koskaan saavuta arvoa 0, eikä arvoja koskaan pudoteta.

Viittauskierron luominen

Katsotaan, miten viittauskierto voi syntyä ja miten se estetään, aloittaen List-enumin määrittelystä ja tail-metodista listauksessa 15-25.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-25/src/main.rs:here}}
}
Listing 15-25: Cons-listan määrittely, joka pitää RefCell<T>:tä, jotta voimme muokata sitä, mihin Cons-variantti viittaa

Käytämme toista variaatiota List-määrittelystä listauksesta 15-5. Cons-variantin toinen elementti on nyt RefCell<Rc<List>>, mikä tarkoittaa, että listauksessa 15-24 tekemämme i32-arvon muuttamisen sijaan haluamme muokata List-arvoa, johon Cons-variantti osoittaa. Lisäämme myös tail-metodin, jotta on kätevää käyttää toista kohdetta, jos meillä on Cons-variantti.

Listauksessa 15-26 lisäämme main-funktion, joka käyttää listauksen 15-25 määrittelyjä. Tämä koodi luo listan a:ssa ja listan b:ssä, joka osoittaa listaan a. Sitten se muokkaa listaa a:ssa osoittamaan b:hen, luoden viittauskierron. Matkan varrella on println!-lauseita, jotka näyttävät viittauslaskurit eri vaiheissa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-26/src/main.rs:here}}
}
Listing 15-26: Kahden toisiinsa osoittavan List-arvon viittauskierron luominen

Luomme Rc<List>-instanssin, joka pitää List-arvoa muuttujassa a alku listalla 5, Nil. Sitten luomme Rc<List>-instanssin, joka pitää toista List-arvoa muuttujassa b, joka sisältää arvon 10 ja osoittaa listaan a.

Muokkaamme a:ta niin, että se osoittaa b:hen Nil:n sijaan, luoden kierron. Teemme tämän käyttämällä tail-metodia saadaksemme viittauksen a:n RefCell<Rc<List>>:iin, jonka sijoitamme muuttujaan link. Sitten käytämme borrow_mut-metodia RefCell<Rc<List>>:ssa muuttaaksemme sisällä olevan arvon Rc<List>:stä, joka pitää Nil-arvoa, b:n Rc<List>:ksi.

Kun ajamme tämän koodin pitäen viimeisen println!:n toistaiseksi kommentoituna, saamme tämän tulosteen:

{{#include ../listings/ch15-smart-pointers/listing-15-26/output.txt}}

Rc<List>-instanssien viittauslaskuri sekä a:ssa että b:ssä on 2 sen jälkeen, kun muutamme listan a:ssa osoittamaan b:hen. main:in lopussa Rust pudottaa muuttujan b, mikä pienentää b:n Rc<List>-instanssin viittauslaskuria 2:sta 1:een. Rc<List>:n keolla olevaa muistia ei pudoteta tässä vaiheessa, koska sen viittauslaskuri on 1, ei 0. Sitten Rust pudottaa a:n, mikä pienentää a:n Rc<List>-instanssin viittauslaskuria myös 2:sta 1:een. Tämän instanssin muistia ei myöskään voida pudottaa, koska toinen Rc<List>-instanssi viittaa siihen edelleen. Listalle varattu muisti jää keräämättä ikuisesti. Visualisoidaksemme tämän viittauskierron olemme luoneet kaavion kuvassa 15-4.

A rectangle labeled 'a' that points to a rectangle containing the integer 5. A rectangle labeled 'b' that points to a rectangle containing the integer 10. The rectangle containing 5 points to the rectangle containing 10, and the rectangle containing 10 points back to the rectangle containing 5, creating a cycle.

Kuva 15-4: Listojen a ja b viittauskierto, jotka osoittavat toisiinsa

Jos poistat viimeisen println!:n kommentoinnin ja ajat ohjelman, Rust yrittää tulostaa tämän kierron a:n osoittaessa b:hen, b:n osoittaessa a:han ja niin edelleen, kunnes pino ylivuotaa.

Verrattuna tosielämän ohjelmaan tämän esimerkin viittauskierron seuraukset eivät ole kovin vakavat: heti viittauskierron luomisen jälkeen ohjelma päättyy. Jos monimutkaisempi ohjelma kuitenkin varaisi paljon muistia kiertoon ja pitäisi sitä kauan, ohjelma käyttäisi enemmän muistia kuin tarvitsisi ja saattaisi ylikuormittaa järjestelmän, jolloin käytettävissä oleva muisti loppuisi.

Viittauskiertojen luominen ei ole helppoa, mutta se ei myöskään ole mahdotonta. Jos sinulla on RefCell<T>-arvoja, jotka sisältävät Rc<T>-arvoja tai vastaavia sisäisen muuttuvuuden ja viittauslaskennan sisäkkäisiä yhdistelmiä, sinun täytyy varmistaa, ettei kiertoja synny; et voi luottaa Rustiin niiden havaitsemisessa. Viittauskierron luominen olisi ohjelmasi logiikkavirhe, jota sinun tulisi minimoida automatisoiduilla testeillä, koodikatselmoinneilla ja muilla ohjelmistokehityskäytännöillä.

Toinen ratkaisu viittauskiertojen välttämiseksi on tietorakenteiden uudelleenjärjestely niin, että jotkut viittaukset ilmaisevat omistajuutta ja jotkut eivät. Näin voit saada kiertoja, jotka koostuvat osittain omistajuussuhteista ja osittain ei-omistajuussuhteista, ja vain omistajuussuhteet vaikuttavat siihen, voidaanko arvo pudottaa. Listauksessa 15-25 haluamme aina, että Cons-variantit omistavat listansa, joten tietorakenteen uudelleenjärjestely ei ole mahdollista. Katsotaan esimerkkiä, jossa käytetään vanhempi- ja lapsisolmuista koostuvia graafeja, nähdäksemme, milloin ei-omistajuussuhteet ovat sopiva tapa estää viittauskiertoja.

Viittauskiertojen estäminen Weak<T>:n avulla

Tähän mennessä olemme osoittaneet, että Rc::clone:n kutsuminen kasvattaa Rc<T>-instanssin strong_count:ia, ja Rc<T>-instanssi siivotaan vain, jos sen strong_count on 0. Voit myös luoda heikon viittauksen Rc<T>-instanssin sisällä olevaan arvoon kutsumalla Rc::downgrade:a ja välittämällä viittauksen Rc<T>:iin. Vahvat viittaukset ovat tapa, jolla voit jakaa Rc<T>-instanssin omistajuuden. Heikot viittaukset eivät ilmaise omistajuussuhdetta, eikä niiden laskuri vaikuta siihen, milloin Rc<T>-instanssi siivotaan. Ne eivät aiheuta viittauskiertoa, koska mikä tahansa kierto, johon liittyy heikkoja viittauksia, katkeaa, kun mukana olevien arvojen vahvan viittauksen laskuri on 0.

Kun kutsut Rc::downgrade:a, saat älykkään osoittimen tyypin Weak<T>. Sen sijaan, että Rc::downgrade kasvattaisi Rc<T>-instanssin strong_count:ia yhdellä, se kasvattaa weak_count:ia yhdellä. Rc<T>-tyyppi käyttää weak_count:ia seuratakseen, kuinka monta Weak<T>-viittausta on olemassa, samoin kuin strong_count:ia. Ero on, että weak_count:in ei tarvitse olla 0, jotta Rc<T>-instanssi siivottaisiin.

Koska arvo, johon Weak<T> viittaa, on saatettu pudottaa, sinun täytyy varmistaa, että arvo on edelleen olemassa, ennen kuin teet mitään arvolla, johon Weak<T> osoittaa. Tee tämä kutsumalla upgrade-metodia Weak<T>-instanssilla, joka palauttaa Option<Rc<T>>:n. Saat tuloksen Some, jos Rc<T>-arvoa ei ole vielä pudotettu, ja tuloksen None, jos Rc<T>-arvo on pudotettu. Koska upgrade palauttaa Option<Rc<T>>:n, Rust varmistaa, että Some- ja None-tapaukset käsitellään, eikä virheellistä osoitinta synny.

Esimerkkinä sen sijaan, että käyttäisimme listaa, jonka kohteet tietävät vain seuraavasta kohteesta, luomme puun, jonka kohteet tietävät sekä lapsikohteistaan että vanhemmistaan.

Puutietorakenteen luominen

Aloitamme rakentamalla puun, jonka solmut tietävät lapsisolmuistaan. Luomme structin nimeltä Node, joka pitää oman i32-arvonsa sekä viittauksia lapsisolmujensa Node-arvoihin:

Tiedostonimi: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:here}}
}

Haluamme, että Node omistaa lapsensa, ja haluamme jakaa tämän omistajuuden muuttujien kanssa, jotta voimme käyttää jokaista puun Node:a suoraan. Tätä varten määrittelemme Vec<T>-kohteet tyyppiä Rc<Node>. Haluamme myös muokata sitä, mitkä solmut ovat toisen solmun lapsia, joten children:issa on RefCell<T> Vec<Rc<Node>>:n ympärillä.

Seuraavaksi käytämme struct-määrittelyämme ja luomme yhden Node-instanssin nimeltä leaf arvolla 3 ilman lapsia, ja toisen instanssin nimeltä branch arvolla 5 ja leaf yhtenä lapsenaan, kuten listauksessa 15-27.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:there}}
}
Listing 15-27: leaf-solmun luominen ilman lapsia ja branch-solmun luominen, jolla leaf on yksi lapsistaan

Kloonaamme Rc<Node>:n leaf:ssa ja tallennamme sen branch:iin, mikä tarkoittaa, että leaf:n Node:lla on nyt kaksi omistajaa: leaf ja branch. Voimme päästä leaf:iin branch:in kautta branch.children:in avulla, mutta emme voi päästä branch:iin leaf:stä. Syy on, että leaf:llä ei ole viittausta branch:iin eikä se tiedä niiden olevan suhteessa toisiinsa. Haluamme leaf:n tietävän, että branch on sen vanhempi. Teemme sen seuraavaksi.

Viittauksen lisääminen lapsesta vanhempaan

Jotta lapsisolmu tietäisi vanhempansa, meidän täytyy lisätä parent-kenttä Node-structin määrittelyyn. Ongelma on päättää, mikä parent:in tyypin pitäisi olla. Tiedämme, ettei se voi sisältää Rc<T>:tä, koska se loisi viittauskierron leaf.parent:in osoittaessa branch:iin ja branch.children:in osoittaessa leaf:iin, mikä saisi niiden strong_count-arvot pysymään ikuisesti muussa kuin nollassa.

Ajatellaan suhteita toisella tavalla: vanhempisolmun pitäisi omistaa lapsensa — jos vanhempisolmu pudotetaan, sen lapsisolmut pitäisi pudottaa myös. Lapsisolmun ei kuitenkaan pitäisi omistaa vanhempaansa: jos pudotamme lapsisolmun, vanhemman pitäisi silti olla olemassa. Tämä on tapaus heikoille viittauksille!

Sen sijaan, että käyttäisimme Rc<T>:tä, teemme parent:in tyypiksi Weak<T>:n, tarkemmin RefCell<Weak<Node>>:n. Nyt Node-structin määrittely näyttää tältä:

Tiedostonimi: src/main.rs

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:here}}
}

Solmu voi viitata vanhempisolmuunsa, mutta se ei omista vanhempaansa. Listauksessa 15-28 päivitämme main:ia käyttämään tätä uutta määrittelyä, jotta leaf-solmulla on tapa viitata vanhempaansa, branch:iin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:there}}
}
Listing 15-28: leaf-solmu, jolla on heikko viittaus vanhempisolmuunsa, branch:iin

leaf-solmun luominen näyttää samalta kuin listauksessa 15-27, paitsi parent-kentän osalta: leaf:lla ei ole aluksi vanhempaa, joten luomme uuden tyhjän Weak<Node>-viittausinstanssin.

Tässä vaiheessa, kun yritämme saada viittauksen leaf:n vanhempaan upgrade-metodilla, saamme arvon None. Näemme tämän ensimmäisen println!-lauseen tulosteesta:

leaf parent = None

Kun luomme branch-solmun, sillä on myös uusi Weak<Node>-viittaus parent-kentässä, koska branch:lla ei ole vanhempisolmua. Meillä on edelleen leaf yhtenä branch:in lapsista. Kun meillä on Node-instanssi branch:issa, voimme muokata leaf:ia antamaan sille Weak<Node>-viittauksen vanhempaansa. Käytämme borrow_mut-metodia leaf:n parent-kentän RefCell<Weak<Node>>:ssa, ja sitten käytämme Rc::downgrade-funktiota luodaksemme Weak<Node>-viittauksen branch:iin branch:in Rc<Node>:sta.

Kun tulostamme leaf:n vanhemman uudelleen, saamme tällä kertaa Some-variantin, joka pitää branch:ia: nyt leaf voi käyttää vanhempaansa! Kun tulostamme leaf:n, vältämme myös kierron, joka lopulta päättyi pinon ylivuotoon kuten listauksessa 15-26; Weak<Node>-viittaukset tulostetaan muodossa (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

Loputtoman tulosteen puuttuminen osoittaa, että tämä koodi ei luonut viittauskiertoa. Voimme myös päätellä tämän katsomalla arvoja, jotka saamme kutsumalla Rc::strong_count:ia ja Rc::weak_count:ia.

strong_count:in ja weak_count:in muutosten visualisointi

Katsotaan, miten Rc<Node>-instanssien strong_count- ja weak_count-arvot muuttuvat luomalla uuden sisäisen näkyvyysalueen ja siirtämällä branch:in luonnin kyseiseen näkyvyysalueeseen. Näin voimme nähdä, mitä tapahtuu, kun branch luodaan ja sitten pudotetaan poistuessaan näkyvyysalueelta. Muutokset on esitetty listauksessa 15-29.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-29/src/main.rs:here}}
}
Listing 15-29: branch:in luominen sisäisessä näkyvyysalueessa ja vahvojen ja heikkojen viittausten lukumäärien tarkastelu

Kun leaf on luotu, sen Rc<Node>:lla on vahva laskuri 1 ja heikko laskuri 0. Sisäisessä näkyvyysalueessa luomme branch:in ja yhdistämme sen leaf:iin, jolloin kun tulostamme laskurit, branch:in Rc<Node>:lla on vahva laskuri 1 ja heikko laskuri 1 (leaf.parent:in osoittaessa branch:iin Weak<Node>:lla). Kun tulostamme laskurit leaf:ssä, näemme, että sillä on vahva laskuri 2, koska branch:illa on nyt klooni leaf:n Rc<Node>:sta tallennettuna branch.children:iin, mutta heikko laskuri on edelleen 0.

Kun sisäinen näkyvyysalue päättyy, branch poistuu näkyvyysalueelta ja Rc<Node>:n vahva laskuri pienenee 0:aan, joten sen Node pudotetaan. Heikko laskuri 1 leaf.parent:ista ei vaikuta siihen, pudotetaanko Node, joten emme saa muistivuotoja!

Jos yritämme käyttää leaf:n vanhempaa näkyvyysalueen päättymisen jälkeen, saamme jälleen None:n. Ohjelman lopussa leaf:n Rc<Node>:lla on vahva laskuri 1 ja heikko laskuri 0, koska muuttuja leaf on nyt ainoa viittaus Rc<Node>:iin.

Kaikki laskureita ja arvojen pudottamista hallitseva logiikka on sisäänrakennettu Rc<T>:ään ja Weak<T>:ään sekä niiden Drop-traitin toteutuksiin. Määrittämällä, että lapsen ja vanhemman välisen suhteen pitäisi olla Weak<T>-viittaus Node:n määrittelyssä, voit saada vanhempisolmut osoittamaan lapsisolmuihin ja päinvastoin luomatta viittauskiertoa ja muistivuotoja.

Yhteenveto

Tässä luvussa käsiteltiin, miten älykkäitä osoittimia käytetään tekemään erilaisia takuita ja kompromisseja verrattuna Rustin oletuksiin tavallisten viittausten kanssa. Box<T>-tyypillä on tunnettu koko ja se osoittaa keolle varattuun dataan. Rc<T>-tyyppi seuraa viittausten määrää keon datassa, jotta datalla voi olla useita omistajia. RefCell<T>-tyyppi sisäisellä muuttuvuudellaan antaa tyypin, jota voimme käyttää, kun tarvitsemme muuttumatonta tyyppiä mutta meidän täytyy muuttaa sen sisäistä arvoa; se myös pakottaa lainausperiaatteet ajonaikana käännösaikaan sijaan.

Käsiteltiin myös Deref- ja Drop-traitit, jotka mahdollistavat suuren osan älykkäiden osoittimien toiminnallisuudesta. Tutustuimme viittauskiertoihin, jotka voivat aiheuttaa muistivuotoja, ja siihen, miten ne estetään Weak<T>:n avulla.

Jos tämä luku herätti kiinnostuksesi ja haluat toteuttaa omat älykkäät osoittimesi, tutustu „The Rustonomicon”-teokseen saadaksesi lisää hyödyllistä tietoa.

Seuraavaksi käsittelemme rinnakkaisuutta Rustissa. Opit jopa muutamasta uudesta älykkäästä osoittimesta.

Peloton rinnakkaisuus

Rinnakkaisen ohjelmoinnin turvallinen ja tehokas käsittely on yksi Rustin tärkeimmistä tavoitteista. Rinnakkainen ohjelmointi, jossa ohjelman eri osat suoritetaan itsenäisesti, ja rinnakkaisohjelmointi, jossa ohjelman eri osat suoritetaan samanaikaisesti, ovat yhä tärkeämpiä, kun useammat tietokoneet hyödyntävät moniydinsuorittimiaan. Historiallisesti ohjelmointi näissä yhteyksissä on ollut vaikeaa ja virhealtista. Rust pyrkii muuttamaan tämän.

Aluksi Rustin kehitystiimi ajatteli, että muistin turvallisuuden varmistaminen ja rinnakkaisuusongelmien estäminen olivat kaksi erillistä haastetta, jotka pitäisi ratkaista eri menetelmillä. Ajan myötä tiimi havaitsi, että omistajuus- ja tyyppijärjestelmät ovat tehokkaita työkaluja sekä muistin turvallisuuden että rinnakkaisuusongelmien hallintaan! Hyödyntämällä omistajuutta ja tyyppitarkistuksia monet rinnakkaisuusvirheet Rustissa ovat kääntöaikaisia virheitä eivätkä ajonaikaisia virheitä.

Tämän ansiosta sinun ei tarvitse käyttää paljon aikaa ajonaikaisen rinnakkaisuusvirheen toistamiseen – virheellinen koodi ei yksinkertaisesti käänny ja antaa virheilmoituksen, joka selittää ongelman. Näin voit korjata koodisi jo kehitysvaiheessa, sen sijaan että joutuisit selvittämään ongelmia vasta tuotannossa. Olemme nimenneet tämän Rustin ominaisuuden pelottomaksi rinnakkaisuudeksi.

Peloton rinnakkaisuus mahdollistaa koodin kirjoittamisen ilman hienovaraisia virheitä ja tekee refaktoroinnista helpompaa ilman uusien virheiden syntymistä.

Huom: Yksinkertaisuuden vuoksi käytämme tässä luvussa termiä rinnakkainen tarkoittamaan sekä rinnakkaista että samanaikaista suorittamista. Mikäli tämä kirja käsittelisi erityisesti rinnakkaisuutta ja/tai samanaikaisuutta, käyttäisimme tarkempaa terminologiaa. Tässä luvussa voit mielessäsi korvata sanan rinnakkainen termillä rinnakkainen ja/tai samanaikainen.

Monet ohjelmointikielet tarjoavat vain tiettyjä ratkaisuja rinnakkaisuusongelmiin. Esimerkiksi Erlang tukee viestinvälitykseen perustuvaa rinnakkaisuutta, mutta tilan jakaminen säikeiden välillä on siinä monimutkaista. Korkean tason ohjelmointikielille on järkevää tarjota vain rajoitettu joukko ratkaisuja, koska ne hyödyntävät abstraktioita ja luopuvat osasta kontrollia helpottaakseen kehittämistä.

Matalamman tason ohjelmointikieliltä odotetaan kuitenkin parempaa suorituskykyä eri tilanteissa ja vähemmän abstraktioita laitteiston päällä. Siksi Rust tarjoaa joukon erilaisia työkaluja, joiden avulla voit valita parhaan ratkaisun omiin tarpeisiisi ja ohjelmointikontekstiisi.

Tässä luvussa käsittelemme seuraavia aiheita:

  • Kuinka luoda säikeitä suorittamaan useita koodin osia samanaikaisesti
  • Viestinvälitykseen perustuva rinnakkaisuus, jossa säikeet lähettävät viestejä toisilleen kanavien kautta
  • Jaetun tilan rinnakkaisuus, jossa useat säikeet voivat käyttää samaa tietoa
  • Sync- ja Send-traitit, jotka laajentavat Rustin rinnakkaisuustakuut koskemaan sekä standardikirjaston että käyttäjän itse määrittelemiä tyyppejä

Säikeiden käyttö koodin samanaikaiseen suorittamiseen

Säikeiden käyttö koodin samanaikaiseen suorittamiseen

Useimmissa nykyisissä käyttöjärjestelmissä suoritetun ohjelman koodi ajetaan prosessissa, ja käyttöjärjestelmä hallinnoi useita prosesseja samanaikaisesti. Ohjelman sisällä voit myös olla itsenäisiä osia, jotka suorittavat tehtäviään samanaikaisesti. Näitä itsenäisiä osia suorittavia ominaisuuksia kutsutaan säikeiksi. Esimerkiksi verkkopalvelimella voi olla useita säikeitä, jotta se voi vastata useampaan kuin yhteen pyyntöön samanaikaisesti.

Ohjelmasi laskennan jakaminen useisiin säikeisiin useiden tehtävien samanaikaiseen suorittamiseen voi parantaa suorituskykyä, mutta se lisää myös monimutkaisuutta. Koska säikeet voivat suorittaa tehtäviään samanaikaisesti, ei ole luontaista takeetta siitä, missä järjestyksessä koodin eri osat eri säikeillä suoritetaan. Tämä voi johtaa ongelmiin, kuten:

  • Kilpailutilanteisiin (race conditions), joissa säikeet käyttävät dataa tai resursseja epäjohdonmukaisessa järjestyksessä
  • Lukoittumisiin (deadlocks), joissa kaksi säiettä odottaa toisiaan, estäen molempia jatkamasta
  • Vikoihin, jotka ilmenevät vain tietyissä tilanteissa ja joita on vaikea toistaa ja korjata luotettavasti

Rust yrittää lieventää säikeiden käytön haitallisia vaikutuksia, mutta monisäikeisessä kontekstissa ohjelmointi vaatii silti huolellista pohdintaa ja koodirakennetta, joka poikkeaa yksisäikeisissä ohjelmissa käytetystä.

Ohjelmointikielet toteuttavat säikeet eri tavoin, ja monet käyttöjärjestelmät tarjoavat ohjelmointirajapinnan (API), jota kieli voi kutsua uusien säikeiden luomiseksi. Rustin standardikirjasto käyttää 1:1-säiemallia, jossa ohjelma käyttää yhtä käyttöjärjestelmän säiettä jokaista kielen säiettä kohden. On olemassa kirjastoja, jotka toteuttavat muita säiemalleja ja tekevät erilaisia kompromisseja verrattuna 1:1-malliin. (Rustin asynkroninen järjestelmä, jota käsittelemme seuraavassa luvussa, tarjoaa toisen lähestymistavan rinnakkaisuuteen.)

Uuden säikeen luominen spawn-funktiolla

Uuden säikeen luomiseksi kutsumme thread::spawn-funktiota ja annamme sille sulkeisen (sulkeisista puhuttiin luvussa 13), joka sisältää uudessa säikeessä ajettavan koodin. Esimerkissä 16-1 pääsäie tulostaa tekstiä ja samanaikaisesti luotu säie tulostaa omaa tekstiään.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-01/src/main.rs}}
}
Listing 16-1: Uuden säikeen luominen tulostamaan yhtä asiaa, kun pääsäie tulostaa jotain muuta

Huomaa, että kun Rust-ohjelman pääsäie päättyy, kaikki luodut säikeet lopetetaan, riippumatta siitä, ovatko ne suorittaneet kaiken työnsä loppuun. Tämän ohjelman tuloste voi olla hieman erilainen joka kerralla, mutta se näyttää suunnilleen tältä:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Kutsut thread::sleep-funktioon pakottavat säikeen pysähtymään hetkeksi, antaen toiselle säikeelle mahdollisuuden suorittaa tehtäviään. Säikeet todennäköisesti vuorottelevat, mutta tätä ei voida taata: se riippuu käyttöjärjestelmän säikeiden ajoituksesta. Tässä ajossa pääsäie tulosti ensin, vaikka luodun säikeen tulostuslauseke on koodissa ensimmäisenä. Ja vaikka käskimme luodun säikeen tulostaa, kunnes i saavuttaa arvon 9, se pääsi vain 5:een ennen kuin pääsäie lopetti ohjelman suorittamisen.

Jos ajat tämän koodin ja näet tulostetta vain pääsäikeeltä, tai et näe päällekkäisyyttä lainkaan, kokeile kasvattaa alueiden numeroita luodaksesi enemmän mahdollisuuksia käyttöjärjestelmälle vaihtaa säikeiden välillä.

Kaikkien säikeiden odottaminen

Listauksen 16-1 koodi ei ainoastaan lopeta luotua säiettä ennenaikaisesti useimmiten pääsäikeen päättymisen vuoksi, vaan koska säikeiden suoritusjärjestyksestä ei ole takeetta, emme voi myöskään taata, että luotu säie ehtii suorittaa lainkaan!

Voimme korjata ongelman, jossa luotu säie ei ehdi suorittaa tai päättyy ennenaikaisesti, tallentamalla thread::spawn-funktion paluuarvon muuttujaan. thread::spawn-funktion paluutyyppi on JoinHandle<T>. JoinHandle<T> on omistettu arvo, joka kun kutsumme sen join-metodia, odottaa säikeen päättymistä. Listausta 16-2 näyttää, miten käytämme listauksessa 16-1 luodun säikeen JoinHandle<T>-kahvaa ja kutsumme join-metodia varmistaaksemme, että luotu säie päättyy ennen kuin main lopettaa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-02/src/main.rs}}
}
Listing 16-2: JoinHandle<T>-kahvan tallentaminen thread::spawn-funktiosta varmistaaksemme, että säie ajetaan loppuun

join-metodin kutsuminen kahvassa estää (blocks) tällä hetkellä suoritettavan säikeen, kunnes kahvan edustama säie päättyy. Säikeen estäminen tarkoittaa, että säie ei voi suorittaa työtä tai lopettaa. Koska olemme sijoittaneet join-kutsun pääsäikeen for-silmukan jälkeen, listauksen 16-2 ajaminen pitäisi tuottaa suunnilleen tämänkaltaisen tulosteen:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

Kaksi säiettä jatkavat vuorottelemista, mutta pääsäie odottaa handle.join()-kutsun vuoksi eikä lopeta ennen kuin luotu säie on valmis.

Mutta katsotaanpa, mitä tapahtuu, jos siirrämme handle.join()-kutsun main-funktion for-silmukan ennen, näin:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/no-listing-01-join-too-early/src/main.rs}}
}

Pääsäie odottaa luodun säikeen päättymistä ja ajaa sitten for-silmukkansa, joten tuloste ei enää vuorottele, kuten tässä:

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Pienillä yksityiskohdilla, kuten siitä, missä join kutsutaan, voi olla vaikutusta siihen, suoritetaanko säikeitä samanaikaisesti vai ei.

move-sulkeisten käyttö säikeiden kanssa

Käytämme usein move-avainsanaa thread::spawn-funktiolle välitettyjen sulkeisten kanssa, koska sulkeis ottaa silloin omistajuuden käyttämistään arvoista ympäristöstään, siirtäen näiden arvojen omistajuuden säikeestä toiseen. ”Viitteiden kaappaaminen tai omistajuuden siirtäminen” -osiossa luvussa 13 käsittelimme move-avainsanaa sulkeisten kontekstissa. Nyt keskitymme enemmän move-avainsanan ja thread::spawn-funktion vuorovaikutukseen.

Huomaa listauksessa 16-1, että thread::spawn-funktiolle välittämämme sulkeis ei ota parametreja: emme käytä mitään dataa pääsäikeestä luodun säikeen koodissa. Käyttääksemme dataa pääsäikeestä luodussa säikeessä luodun säikeen sulkeisen täytyy kaapata tarvitsemansa arvot. Listausta 16-3 näyttää yrityksen luoda vektori pääsäikeessä ja käyttää sitä luodussa säikeessä. Tämä ei kuitenkaan vielä toimi, kuten näet hetken kuluttua.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-03/src/main.rs}}
Listing 16-3: Yritys käyttää pääsäikeen luomaa vektoria toisessa säikeessä

Sulkeis käyttää v-muuttujaa, joten se kaappaa v:n ja tekee siitä osan sulkeisen ympäristöä. Koska thread::spawn suorittaa tämän sulkeisen uudessa säikeessä, meidän pitäisi pystyä käyttämään v:tä kyseisessä uudessa säikeessä. Mutta kun käännetään tämä esimerkki, saamme seuraavan virheen:

{{#include ../listings/ch16-fearless-concurrency/listing-16-03/output.txt}}

Rust päättelee, miten v kaapataan, ja koska println! tarvitsee vain viitteen v:hen, sulkeis yrittää lainata v:tä. Ongelma on kuitenkin se, että Rust ei tiedä, kuinka kauan luotu säie ajaa, joten se ei tiedä, onko viite v:hen aina kelvollinen.

Listausta 16-4 tarjoaa skenaarion, jossa viite v:hen ei todennäköisesti ole kelvollinen.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-04/src/main.rs}}
Listing 16-4: Säie, jonka sulkeis yrittää kaapata viitteen v:hen pääsäikeestä, joka pudottaa v:n

Jos Rust sallisi tämän koodin ajamisen, olisi mahdollista, että luotu säie laitettaisiin heti taustalle ajamatta lainkaan. Luodulla säikeellä on viite v:hen sisällään, mutta pääsäie pudottaa v:n heti käyttäen luvussa 15 käsittelemäämme drop-funktiota. Sitten kun luotu säie alkaa suorittaa, v ei ole enää kelvollinen, joten viite siihen on myös virheellinen. Voi ei!

Korjataksemme listauksen 16-3 kääntäjävirheen voimme käyttää virheilmoituksen neuvoa:

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

Lisäämällä move-avainsanan ennen sulkeista pakotamme sulkeisen ottamaan omistajuuden käyttämistään arvoista sen sijaan, että antaisimme Rustin päättää, että sen pitäisi lainata arvoja. Listausta 16-3 vastaava muutos listauksessa 16-5 kääntyy ja toimii tarkoitetulla tavalla.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-05/src/main.rs}}
}
Listing 16-5: move-avainsanan käyttö pakottaaksemme sulkeisen ottamaan omistajuuden käyttämistään arvoista

Saatamme olla halukkaita kokeilemaan samaa korjausta listauksen 16-4 koodiin, jossa pääsäie kutsui drop-funktiota käyttämällä move-sulkeista. Tämä korjaus ei kuitenkaan toimi, koska se, mitä listaus 16-4 yrittää tehdä, on kielletty eri syystä. Jos lisäisimme move-avainsanan sulkeiseen, siirtäisimme v:n sulkeisen ympäristöön emmekä voisi enää kutsua drop-funktiota pääsäikeessä. Saisimme sen sijaan tämän kääntäjävirheen:

{{#include ../listings/ch16-fearless-concurrency/output-only-01-move-drop/output.txt}}

Rustin omistajuussäännöt pelastivat meidät jälleen! Saimme virheen listauksen 16-3 koodista, koska Rust oli varovainen ja lainasi v:tä vain säikeelle, mikä tarkoitti, että pääsäie saattoi teoriassa mitätöidä luodun säikeen viitteen. Kertomalla Rustille siirtämään v:n omistajuuden luodulle säikeelle takaamme Rustille, että pääsäie ei enää käytä v:tä. Jos muuttaisimme listauksen 16-4 samalla tavalla, rikkoisimme omistajuussääntöjä yrittäessämme käyttää v:tä pääsäikeessä. move-avainsana ohittaa Rustin varovaisen oletuslainaamisen; se ei anna meidän rikkoa omistajuussääntöjä.

Nyt kun olemme käsitelleet säikeet ja säikeiden API:n tarjoamat metodit, katsotaan joitakin tilanteita, joissa säikeitä voi käyttää.

Viestien välityksen käyttö tietojen siirtämiseen säikeiden välillä

Datan siirtäminen säikeiden välillä viestinvälityksellä

Yhä suositumpi lähestymistapa turvallisen rinnakkaisuuden varmistamiseen on viestinvälitys, jossa säikeet tai aktorit kommunikoivat lähettämällä toisilleen viestejä, jotka sisältävät dataa. Tässä on ajatus Go-ohjelmointikielen dokumentaation iskulauseessa: ”Älä kommunikoi jakamalla muistia; sen sijaan jaa muisti kommunikoimalla.”

Viestinvälitykseen perustuvan rinnakkaisuuden toteuttamiseksi Rustin standardikirjasto tarjoaa kanavien toteutuksen. Kanava on yleinen ohjelmointikäsite, jolla data lähetetään säikeestä toiseen.

Voit kuvitella ohjelmoinnissa kanavan suuntaiseksi vesiväyläksi, kuten puroksi tai joeksi. Jos laitat esimerkiksi kumiankan vesiväylään, se kulkee alavirtaan vesiväylän päähän.

Kanavalla on kaksi puoliskoa: lähettäjä ja vastaanotin. Lähettäjäpuoli on ylävirran kohta, johon laitat kumiankan jokeen, ja vastaanotinpuoli on kohta, johon kumiankka päätyy alavirtaan. Ohjelmasi yksi osa kutsuu lähettäjän metodeja datalla, jonka haluat lähettää, ja toinen osa tarkistaa vastaanottopään saapuvat viestit. Kanavaa sanotaan suljetuksi, jos jompikumpi lähettäjä- tai vastaanotinpuolisko pudotetaan.

Tässä rakennamme ohjelman, jossa yksi säie tuottaa arvoja ja lähettää ne kanavaa pitkin, ja toinen säie vastaanottaa arvot ja tulostaa ne. Lähetämme yksinkertaisia arvoja säikeiden välillä kanavan avulla havainnollistamaan ominaisuutta. Kun olet tutustunut tekniikkaan, voit käyttää kanavia minkä tahansa toisiinsa kommunikoivien säikeiden välillä, kuten chat-järjestelmässä tai järjestelmässä, jossa useat säikeet suorittavat osia laskennasta ja lähettävät osat yhdelle säikeelle, joka kokoaa tulokset.

Ensin listauksessa 16-6 luomme kanavan, mutta emme tee sillä mitään. Huomaa, että tämä ei vielä käänny, koska Rust ei tiedä, millaisia arvoja haluamme lähettää kanavan kautta.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-06/src/main.rs}}
Listing 16-6: Kanavan luominen ja kahden puoliskon sijoittaminen muuttujiin tx ja rx

Luomme uuden kanavan mpsc::channel-funktiolla; mpsc tarkoittaa multiple producer, single consumer (useita tuottajia, yksi kuluttaja). Lyhyesti sanottuna Rustin standardikirjaston kanavatoteutus tarkoittaa, että kanavalla voi olla useita lähettäviä päitä, jotka tuottavat arvoja, mutta vain yksi vastaanottava pää, joka kuluttaa nämä arvot. Kuvittele useita puroja, jotka virtaavat yhteen suureen jokeen: kaikki mihin tahansa puroon lähetetty päätyy lopulta yhteen jokeen. Aloitamme yhdellä tuottajalla, mutta lisäämme useita tuottajia, kun saamme tämän esimerkin toimimaan.

mpsc::channel-funktio palauttaa monikon, jonka ensimmäinen elementti on lähettävä pää – lähettäjä – ja toinen elementti on vastaanottava pää – vastaanotin. Lyhenteitä tx ja rx käytetään perinteisesti monilla aloilla tarkoittamaan transmitter (lähettäjä) ja receiver (vastaanotin), joten nimeämme muuttujamme näin osoittaaksemme kumman pään kyseessä on. Käytämme let-lauseketta kuviolla, joka purkaa monikon; käsittelemme kuvioiden käyttöä let-lausekkeissa ja purkamista luvussa 19. Toistaiseksi riittää tietää, että let-lausekkeen käyttö tällä tavalla on kätevä tapa erottaa mpsc::channel-funktion palauttaman monikon osat.

Siirretään lähettävä pää luotuun säikeeseen ja lähetetään yksi merkkijono, jotta luotu säie kommunikoi pääsäikeen kanssa, kuten listauksessa 16-7 näytetään. Tämä on kuin laittaisit kumiankan jokeen ylävirtaan tai lähettäisit chat-viestin säikeestä toiseen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-07/src/main.rs}}
}
Listing 16-7: tx:n siirtäminen luotuun säikeeseen ja "hi"-merkkijonon lähettäminen

Käytämme jälleen thread::spawn-funktiota luodaksemme uuden säikeen ja käytämme move-avainsanaa siirtääksemme tx:n sulkeiseen, jotta luotu säie omistaa tx:n. Luodun säikeen täytyy omistaa lähettäjä, jotta se voi lähettää viestejä kanavan kautta.

Lähettäjällä on send-metodi, joka ottaa lähetettävän arvon. send-metodi palauttaa tyypin Result<T, E>, joten jos vastaanotin on jo pudotettu eikä arvoa voi lähettää minnekään, lähetysoperaatio palauttaa virheen. Tässä esimerkissä kutsumme unwrap-metodia panikoidaksemme virheen sattuessa. Todellisessa sovelluksessa käsittelisimme sen asianmukaisesti: palaa lukuun 9 tarkastelemaan asianmukaista virheenkäsittelyä.

Listauksessa 16-8 haemme arvon vastaanottajalta pääsäikeessä. Tämä on kuin noutaisit kumiankan vedestä joen päästä tai vastaanottaisit chat-viestin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-08/src/main.rs}}
}
Listing 16-8: Arvon "hi" vastaanottaminen pääsäikeessä ja sen tulostaminen

Vastaanottajalla on kaksi hyödyllistä metodia: recv ja try_recv. Käytämme recv-metodia, lyhenne sanasta receive (vastaanottaa), joka estää pääsäikeen suorituksen ja odottaa, kunnes arvo lähetetään kanavaa pitkin. Kun arvo on lähetetty, recv palauttaa sen tyypissä Result<T, E>. Kun lähettäjä sulkeutuu, recv palauttaa virheen ilmaistakseen, ettei enempää arvoja ole tulossa.

try_recv-metodi ei estä suoritusta, vaan palauttaa tyypin Result<T, E> heti: Ok-arvon, joka sisältää viestin, jos sellainen on saatavilla, ja Err-arvon, jos viestejä ei tällä kertaa ole. try_recv-metodin käyttö on hyödyllistä, jos tällä säikeellä on muuta työtä tehtävänä viestien odottamisen ohella: voisimme kirjoittaa silmukan, joka kutsuu try_recv-metodia aika ajoin, käsittelee viestin, jos sellainen on saatavilla, ja muuten tekee muuta työtä hetken ennen uutta tarkistusta.

Olemme käyttäneet tässä esimerkissä recv-metodia yksinkertaisuuden vuoksi; pääsäikeellä ei ole muuta työtä kuin viestien odottaminen, joten pääsäikeen estäminen on asianmukaista.

Kun ajamme listauksen 16-8 koodin, näemme arvon tulostettuna pääsäikeestä:

Got: hi

Täydellistä!

Omistajuuden siirtäminen kanavien kautta

Omistajuussäännöillä on keskeinen rooli viestien lähettämisessä, koska ne auttavat kirjoittamaan turvallista rinnakkaista koodia. Virheiden estäminen rinnakkaisohjelmoinnissa on etu, joka syntyy omistajuuden huomioimisesta koko Rust-ohjelmassasi. Tehdään koe, joka näyttää, miten kanavat ja omistajuus toimivat yhdessä estäen ongelmia: yritämme käyttää val-arvoa luodussa säikeessä sen jälkeen, kun olemme lähettäneet sen kanavaa pitkin. Kokeile kääntää listauksen 16-9 koodi nähdäksesi, miksi tämä koodi ei ole sallittu.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-09/src/main.rs}}
Listing 16-9: Yritys käyttää val-arvoa sen jälkeen, kun olemme lähettäneet sen kanavaa pitkin

Tässä yritämme tulostaa val-arvon sen jälkeen, kun olemme lähettäneet sen kanavaa pitkin tx.send-kutsulla. Tämän salliminen olisi huono ajatus: kun arvo on lähetetty toiseen säikeeseen, kyseinen säie voisi muokata tai pudottaa sen ennen kuin yritämme käyttää arvoa uudelleen. Toisen säikeen muutokset voisivat aiheuttaa virheitä tai odottamattomia tuloksia epäjohdonmukaisen tai olemattoman datan vuoksi. Rust antaa kuitenkin virheen, jos yritämme kääntää listauksen 16-9 koodin:

{{#include ../listings/ch16-fearless-concurrency/listing-16-09/output.txt}}

Rinnakkaisuusvirheemme aiheutti kääntöaikaisen virheen. send-funktio ottaa omistajuuden parametristaan, ja kun arvo siirretään, vastaanotin ottaa sen omistukseensa. Tämä estää meitä vahingossa käyttämästä arvoa uudelleen lähettämisen jälkeen; omistajuusjärjestelmä tarkistaa, että kaikki on kunnossa.

Useiden arvojen lähettäminen

Listauksen 16-8 koodi kääntyi ja toimi, mutta se ei selvästi näyttänyt, että kaksi erillistä säiettä kommunikoi kanavan kautta.

Listauksessa 16-10 olemme tehneet muutoksia, jotka todistavat listauksen 16-8 koodin toimivan rinnakkain: luotu säie lähettää nyt useita viestejä ja pysähtyy sekunniksi jokaisen viestin välillä.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-10/src/main.rs}}
Listing 16-10: Useiden viestien lähettäminen ja tauko jokaisen välillä

Tällä kertaa luodulla säikeellä on merkkijonovektori, jonka haluamme lähettää pääsäikeeseen. Iteroimme niiden yli lähettäen jokaisen erikseen ja pysähtyen jokaisen välillä kutsumalla thread::sleep-funktiota yhden sekunnin Duration-arvolla.

Pääsäikeessä emme enää kutsu recv-funktiota eksplisiittisesti: sen sijaan käsittelemme rx:ää iteraattorina. Jokaisesta vastaanotetusta arvosta tulostamme sen. Kun kanava sulkeutuu, iteraatio päättyy.

Kun ajat listauksen 16-10 koodin, sinun pitäisi nähdä seuraava tuloste yhden sekunnin tauolla jokaisen rivin välillä:

Got: hi
Got: from
Got: the
Got: thread

Koska for-silmukassamme pääsäikeessä ei ole koodia, joka pysäyttäisi tai viivyttäisi suoritusta, voimme päätellä, että pääsäie odottaa arvoja luodulta säikeeltä.

Useiden tuottajien luominen

Aiemmin mainitsimme, että mpsc on lyhenne sanasta multiple producer, single consumer. Otetaan mpsc käyttöön ja laajennetaan listauksen 16-10 koodia luomalla useita säikeitä, jotka kaikki lähettävät arvoja samalle vastaanottajalle. Voimme tehdä näin kloonaamalla lähettäjän, kuten listauksessa 16-11 näytetään.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-11/src/main.rs:here}}
Listing 16-11: Useiden viestien lähettäminen useilta tuottajilta

Tällä kertaa ennen ensimmäisen säikeen luomista kutsumme clone-metodia lähettäjällä. Tämä antaa meille uuden lähettäjän, jonka voimme välittää ensimmäiselle luodulle säikeelle. Välitämme alkuperäisen lähettäjän toiselle luodulle säikeelle. Näin saamme kaksi säiettä, joista kumpikin lähettää eri viestejä yhdelle vastaanottajalle.

Kun ajat koodin, tulosteesi pitäisi näyttää suunnilleen tältä:

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

Saatat nähdä arvot eri järjestyksessä järjestelmästäsi riippuen. Tämä tekee rinnakkaisuudesta sekä mielenkiintoista että vaikeaa. Jos kokeilet thread::sleep-funktiota antamalla sille erilaisia arvoja eri säikeissä, jokainen ajo on epädeterministisempi ja tuottaa erilaisen tulosteen joka kerta.

Nyt kun olemme tarkastelleet, miten kanavat toimivat, katsotaan toista rinnakkaisuusmenetelmää.

Jaetun tilan rinnakkaisuus

Jaetun tilan rinnakkaisuus

Viestinvälitys on hyvä tapa hallita rinnakkaisuutta, mutta se ei ole ainoa tapa. Toinen menetelmä on, että useat säikeet pääsevät käsiksi samaan jaettuun dataan. Tarkastellaan Go-ohjelmointikielen dokumentaation iskulauseen osaa uudelleen: ”Älä kommunikoi jakamalla muistia.”

Miltä muistia jakamalla kommunikointi näyttäisi? Lisäksi, miksi viestinvälityksen kannattajat varoittavat muistinjaon käytöstä?

Eräänläisesti kanavat missä tahansa ohjelmointikielessä muistuttavat yksinomistajuutta, koska kun siirrät arvon kanavaa pitkin, sinun ei pitäisi enää käyttää sitä arvoa. Jaetun muistin rinnakkaisuus on kuin moniomistajuus: useat säikeet voivat käyttää samaa muistipaikkaa samanaikaisesti. Kuten näit luvussa 15, älykkäät osoittimet mahdollistivat moniomistajuuden, ja moniomistajuus voi lisätä monimutkaisuutta, koska näitä eri omistajia täytyy hallita. Rustin tyyppijärjestelmä ja omistajuussäännöt auttavat merkittävästi tämän hallinnan oikeaksi saamisessa. Esimerkkinä tarkastellaan mutekseja, yhtä yleisimmistä jaetun muistin rinnakkaisuuden primitiiveistä.

Pääsyn hallinta mutekseilla

Mutex on lyhenne sanasta mutual exclusion (keskinäinen poissulkeminen), sillä muteksi sallii vain yhden säikeen käyttää tiettyä dataa milloin tahansa. Päästäkseen käsiksi muteksin suojaamaan dataan säikeen täytyy ensin ilmoittaa haluavansa pääsyn pyytämällä muteksin lukitusta. Lukitus on muteksiin kuuluva tietorakenne, joka pitää kirjaa siitä, kuka tällä hetkellä omistaa yksinoikeudellisen pääsyn dataan. Tästä syystä muteksia kuvataan suojaavan hallussaan olevaa dataa lukitusjärjestelmän avulla.

Mutekseilla on maine vaikeasti käytettävinä, koska sinun täytyy muistaa kaksi sääntöä:

  1. Sinun täytyy yrittää hankkia lukitus ennen datan käyttöä.
  2. Kun olet valmis muteksin suojaaman datan kanssa, sinun täytyy vapauttaa lukitus, jotta muut säikeet voivat hankkia lukituksen.

Todellisen maailman vertauskuva mutekseille on paneelikeskustelu konferenssissa, jossa on vain yksi mikrofoni. Ennen kuin paneelisti voi puhua, hänen täytyy pyytää tai ilmoittaa haluavansa käyttää mikrofonia. Kun hän saa mikrofonin, hän voi puhua niin kauan kuin haluaa ja antaa sitten mikrofonin seuraavalle paneelistille, joka pyytää puheenvuoroa. Jos paneelisti unohtaa luovuttaa mikrofonin, kun hän on valmis, kukaan muu ei voi puhua. Jos jaetun mikrofonin hallinta menee pieleen, paneeli ei toimi suunnitellusti!

Muteksien hallinta voi olla uskomattoman hankalaa saada oikein, minkä vuoksi niin monet ovat innostuneita kanavista. Rustin tyyppijärjestelmän ja omistajuussääntöjen ansiosta et kuitenkaan voi tehdä virheitä lukituksessa ja lukituksen vapauttamisessa.

Mutex<T>-tyypin API

Esimerkkinä muteksin käytöstä aloitetaan muteksin käyttäminen yksisäikeisessä kontekstissa, kuten listauksessa 16-12 näytetään.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-12/src/main.rs}}
}
Listing 16-12: Mutex<T>-tyypin API:n tutkiminen yksisäikeisessä kontekstissa yksinkertaisuuden vuoksi

Kuten monien tyyppien kohdalla, luomme Mutex<T>-tyypin liitetyllä funktiolla new. Päästäksemme käsiksi muteksin sisällä olevaan dataan käytämme lock-metodia lukituksen hankkimiseksi. Tämä kutsu estää nykyisen säikeen, jotta se ei voi tehdä työtä ennen kuin on meidän vuoromme saada lukitus.

lock-kutsu epäonnistuisi, jos toinen säie, joka pitää lukitusta, panikoisi. Tällöin kukaan ei voisi koskaan saada lukitusta, joten olemme valinneet unwrap-metodin ja annamme tämän säikeen panikoida, jos olemme tällaisessa tilanteessa.

Kun olemme hankkineet lukituksen, voimme käsitellä paluuarvoa, jota tässä tapauksessa kutsutaan num-muuttujaksi, muuttuvana viitteenä muteksin sisällä olevaan dataan. Tyyppijärjestelmä varmistaa, että hankimme lukituksen ennen arvon käyttöä m:ssä. m:n tyyppi on Mutex<i32>, ei i32, joten meidän täytyy kutsua lock-metodia voidaksemme käyttää i32-arvoa. Emme voi unohtaa; tyyppijärjestelmä ei anna meidän käyttää sisäistä i32-arvoa muuten.

lock-kutsu palauttaa tyypin nimeltä MutexGuard, joka on kääritty LockResult-tyyppiin ja jonka käsittelimme unwrap-kutsulla. MutexGuard-tyyppi toteuttaa Deref-traitin osoittaakseen sisäiseen dataamme; tyypillä on myös Drop-toteutus, joka vapauttaa lukituksen automaattisesti, kun MutexGuard poistuu laajuudesta, mikä tapahtuu sisemmän laajuuden lopussa. Näin emme vaaranna unohtaa lukituksen vapauttamista ja estää muteksin käyttöä muilla säikeillä, koska lukituksen vapauttaminen tapahtuu automaattisesti.

Lukituksen pudottamisen jälkeen voimme tulostaa muteksin arvon ja nähdä, että pystyimme muuttamaan sisäisen i32-arvon arvoksi 6.

Jaettu pääsy Mutex<T>-tyyppiin

Kokeillaan nyt jakaa arvo useiden säikeiden välillä Mutex<T>-tyypin avulla. Luomme 10 säiettä ja annamme niiden jokaisen kasvattaa laskurin arvoa yhdellä, jotta laskuri menee arvosta 0 arvoon 10. Listauksen 16-13 esimerkissä on kääntäjävirhe, ja käytämme sitä virhettä oppiaksemme lisää Mutex<T>-tyypin käytöstä ja siitä, miten Rust auttaa käyttämään sitä oikein.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-13/src/main.rs}}
Listing 16-13: Kymmenen säiettä, joista jokainen kasvattaa Mutex<T>-tyypin suojaamaa laskuria

Luomme counter-muuttujan, joka sisältää i32-arvon Mutex<T>-tyypin sisällä, kuten teimme listauksessa 16-12. Seuraavaksi luomme 10 säiettä iteroiden lukuarvojen alueen yli. Käytämme thread::spawn-funktiota ja annamme kaikille säikeille saman sulkeisen: sellaisen, joka siirtää laskurin säikeeseen, hankkii lukituksen Mutex<T>-tyyppiin kutsumalla lock-metodia ja lisää sitten 1 muteksin arvoon. Kun säie on suorittanut sulkeisensa loppuun, num poistuu laajuudesta ja vapauttaa lukituksen, jotta toinen säie voi hankkia sen.

Pääsäikeessä keräämme kaikki liittymiskahvat. Sitten, kuten teimme listauksessa 16-2, kutsumme join-metodia jokaisella kahvalla varmistaaksemme, että kaikki säikeet päättyvät. Tässä vaiheessa pääsäie hankkii lukituksen ja tulostaa ohjelman tuloksen.

Vihjasimme, että tämä esimerkki ei käänny. Selvitetään nyt miksi!

{{#include ../listings/ch16-fearless-concurrency/listing-16-13/output.txt}}

Virheilmoitus kertoo, että counter-arvo siirrettiin silmukan edellisessä iteraatiossa. Rust kertoo meille, ettemme voi siirtää counter-lukon omistajuutta useille säikeille. Korjataan kääntäjävirhe moniomistajuusmenetelmällä, josta puhuimme luvussa 15.

Moniomistajuus useilla säikeillä

Luvussa 15 annoimme arvon useille omistajille käyttämällä älykästä osoitinta Rc<T> viittauslaskennan arvon luomiseksi. Tehdään sama tässä ja katsotaan, mitä tapahtuu. Käärimme Mutex<T>-tyypin Rc<T>-tyyppiin listauksessa 16-14 ja kloonaamme Rc<T>-tyypin ennen omistajuuden siirtämistä säikeeseen.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-14/src/main.rs}}
Listing 16-14: Yritys käyttää Rc<T>-tyyppiä salliaksemme useiden säikeiden omistaa Mutex<T>-tyypin

Käännetään jälleen ja saamme… erilaisia virheitä! Kääntäjä opettaa meille paljon:

{{#include ../listings/ch16-fearless-concurrency/listing-16-14/output.txt}}

Vau, tuo virheilmoitus on hyvin pitkä! Tässä on tärkeä osa, johon kannattaa keskittyä: `Rc<Mutex<i32>>` cannot be sent between threads safely. Kääntäjä kertoo myös syyn: the trait `Send` is not implemented for `Rc<Mutex<i32>>`. Puhumme Send-traitista seuraavassa osiossa: se on yksi trateista, jotka varmistavat, että säikeiden kanssa käyttämämme tyypit on tarkoitettu rinnakkaisiin tilanteisiin.

Valitettavasti Rc<T> ei ole turvallinen jaettavaksi säikeiden välillä. Kun Rc<T> hallinnoi viittauslaskentaa, se lisää laskuria jokaisella clone-kutsulla ja vähentää laskuria, kun jokainen klooni pudotetaan. Se ei kuitenkaan käytä rinnakkaisuusprimitiivejä varmistaakseen, ettei toinen säie voi keskeyttää muutoksia laskuriin. Tämä voisi johtaa virheellisiin laskuriarvoihin – hienovaraisiin vikoihin, jotka puolestaan voisivat johtaa muistivuotoihin tai arvon pudottamiseen ennen kuin olemme valmiita sen kanssa. Tarvitsemme tyypin, joka on täsmälleen kuin Rc<T>, mutta joka tekee muutokset viittauslaskuriin säikeistä turvallisella tavalla.

Atominen viittauslaskenta Arc<T>-tyypillä

Onneksi Arc<T> on Rc<T>:n kaltainen tyyppi, joka on turvallinen käyttää rinnakkaisissa tilanteissa. a tarkoittaa atomic (atominen), eli kyseessä on atomisesti viittauslaskettu tyyppi. Atomit ovat eräänlainen rinnakkaisuusprimitiivi, jota emme käsittele tässä yksityiskohtaisesti: katso standardikirjaston dokumentaatio std::sync::atomic -moduulista lisätietoja. Tässä vaiheessa sinun tarvitsee vain tietää, että atomit toimivat kuin primitiivityypit, mutta ne ovat turvallisia jaettavaksi säikeiden välillä.

Saatat sitten ihmetellä, miksi kaikki primitiivityypit eivät ole atomisia ja miksi standardikirjaston tyyppejä ei ole toteutettu käyttämään Arc<T>-tyyppiä oletuksena. Syy on, että säikeistettävyys tuo mukanaan suorituskykyrasitteen, jonka haluat maksaa vain, kun sitä todella tarvitset. Jos suoritat operaatioita arvoihin yhdessä säikeessä, koodisi voi toimia nopeammin, jos sen ei tarvitse pakottaa atomien tarjoamia takeita.

Palataan esimerkkiimme: Arc<T>- ja Rc<T>-tyypeillä on sama API, joten korjaamme ohjelmamme muuttamalla use-riviä, new-kutsua ja clone-kutsua. Listauksen 16-15 koodi kääntyy vihdoin ja toimii.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-15/src/main.rs}}
}
Listing 16-15: Arc<T>-tyypin käyttö Mutex<T>-tyypin käärimiseen omistajuuden jakamiseksi useiden säikeiden välillä

Tämä koodi tulostaa seuraavan:

Result: 10

Onnistuimme! Laskimme arvosta 0 arvoon 10, mikä ei ehkä vaikuta kovin vaikuttavalta, mutta se opetti meille paljon Mutex<T>-tyypistä ja säikeistä turvallisuudesta. Voit myös käyttää tämän ohjelman rakennetta monimutkaisempiin operaatioihin kuin pelkkään laskurin kasvattamiseen. Tällä strategialla voit jakaa laskennan itsenäisiin osiin, jakaa nämä osat säikeille ja käyttää sitten Mutex<T>-tyyppiä, jotta jokainen säie päivittää lopputuloksen omalla osallaan.

Huomaa, että jos suoritat yksinkertaisia numeerisia operaatioita, standardikirjaston std::sync::atomic-moduuli tarjoaa yksinkertaisempia tyyppejä kuin Mutex<T>-tyypit. Nämä tyypit tarjoavat turvallisen, rinnakkaisen, atomisen pääsyn primitiivityyppeihin. Valitsimme tässä esimerkissä Mutex<T>-tyypin primitiivityypin kanssa, jotta voimme keskittyä siihen, miten Mutex<T> toimii.

RefCell<T>/Rc<T>- ja Mutex<T>/Arc<T>-tyyppien vertailu

Saatat olla huomannut, että counter on muuttumaton, mutta pystyimme saamaan muuttuvan viitteen sen sisällä olevaan arvoon; tämä tarkoittaa, että Mutex<T> tarjoaa sisäisen mutabiliteetin, kuten Cell-perhe. Samalla tavalla kuin käytimme RefCell<T>-tyyppiä luvussa 15 salliaksemme sisällön mutatoimisen Rc<T>-tyypin sisällä, käytämme Mutex<T>-tyyppiä sisällön mutatoimiseen Arc<T>-tyypin sisällä.

Toinen huomioitava yksityiskohta on, että Rust ei voi suojella sinua kaikilta loogisilta virheiltä, kun käytät Mutex<T>-tyyppiä. Muista luvusta 15, että Rc<T>-tyypin käyttöön liittyi riski luoda viittauskiertoja, joissa kaksi Rc<T>-arvoa viittaa toisiinsa ja aiheuttaa muistivuotoja. Vastaavasti Mutex<T>-tyyppiin liittyy riski luoda lukoittumisia. Ne syntyvät, kun operaatio tarvitsee lukita kaksi resurssia ja kaksi säiettä on hankkinut kummankin lukituksen, jolloin ne odottavat toisiaan ikuisesti. Jos olet kiinnostunut lukoittumisista, kokeile luoda Rust-ohjelma, jossa on lukoittuminen; tutki sitten lukoittumisten lieventämisstrategioita mutekseille missä tahansa kielessä ja kokeile toteuttaa ne Rustissa. Standardikirjaston API-dokumentaatio Mutex<T>- ja MutexGuard-tyypeille tarjoaa hyödyllistä tietoa.

Päätämme tämän luvun keskustelemalla Send- ja Sync-traiteista ja siitä, miten voimme käyttää niitä mukautettujen tyyppien kanssa.

Laajennettava rinnakkaisuus Send- ja Sync-traitien avulla

Laajennettava rinnakkaisuus Send- ja Sync-traitien avulla

Mielenkiintoista kyllä, lähes kaikki rinnakkaisuusominaisuudet, joista olemme puhuneet tässä luvussa, ovat olleet osa standardikirjastoa, eivät kieltä. Rinnakkaisuuden käsittelyn vaihtoehdot eivät rajoitu kieleen tai standardikirjastoon; voit kirjoittaa omia rinnakkaisuusominaisuuksiasi tai käyttää muiden kirjoittamia.

Kielen sisään upotettuja rinnakkaisuuskäsitteitä ovat kuitenkin muun muassa std::marker-traitit Send ja Sync.

Omistajuuden siirtäminen säikeiden välillä

Send-merkkaustraitti osoittaa, että tätä traitia toteuttavan tyypin arvojen omistajuus voidaan siirtää säikeiden välillä. Lähes jokainen Rustin tyyppi toteuttaa Send-traitin, mutta on olemassa poikkeuksia, kuten Rc<T>: tämä ei voi toteuttaa Send-traitia, koska jos kloonaisit Rc<T>-arvon ja yrittäisit siirtää kloonin omistajuuden toiseen säikeeseen, molemmat säikeet saattaisivat päivittää viittauslaskuria samanaikaisesti. Tästä syystä Rc<T> on toteutettu käytettäväksi yksisäikeisissä tilanteissa, joissa et halua maksaa säikeistettävyyteen liittyvää suorituskykyrasitetta.

Rustin tyyppijärjestelmä ja trait-rajoitukset varmistavat siis, ettei voi vahingossa lähettää Rc<T>-arvoa säikeiden välillä turvattomasti. Kun yritimme tehdä tämän listauksessa 16-14, saimme virheen the trait `Send` is not implemented for `Rc<Mutex<i32>>`. Kun vaihdoimme Arc<T>-tyyppiin, joka toteuttaa Send-traitin, koodi kääntyi.

Mikä tahansa tyyppi, joka koostuu kokonaan Send-tyypeistä, merkitään automaattisesti myös Send-traitilla. Lähes kaikki primitiivityypit ovat Send, lukuun ottamatta raakaviittauksia, joita käsittelemme luvussa 20.

Pääsy useista säikeistä

Sync-merkkaustraitti osoittaa, että tätä traitia toteuttavaan tyyppiin on turvallista viitata useista säikeistä. Toisin sanoen mikä tahansa tyyppi T toteuttaa Sync-traitin, jos &T (muuttumaton viittaus tyyppiin T) toteuttaa Send-traitin, eli viittaus voidaan lähettää turvallisesti toiseen säikeeseen. Samoin kuin Send-traitin kohdalla, primitiivityypit toteuttavat kaikki Sync-traitin, ja tyypit, jotka koostuvat kokonaan Sync-traitia toteuttavista tyypeistä, toteuttavat myös Sync-traitin.

Älykäs osoitin Rc<T> ei myöskään toteuta Sync-traitia samoista syistä kuin se ei toteuta Send-traitia. RefCell<T>-tyyppi (josta puhuimme luvussa 15) ja siihen liittyvä Cell<T>-tyyppien perhe eivät toteuta Sync-traitia. RefCell<T>-tyypin ajonaikainen lainanvalvonta ei ole säikeistä turvallista. Älykäs osoitin Mutex<T> toteuttaa Sync-traitin ja sitä voidaan käyttää jakamaan pääsy useille säikeille, kuten näit ”Jaettu pääsy Mutex<T>-tyyppiin” -osiossa.

Send- ja Sync-traitien manuaalinen toteuttaminen on turvatonta

Koska tyypit, jotka koostuvat kokonaan muista tyypeistä, jotka toteuttavat Send- ja Sync-traitit, toteuttavat automaattisesti myös Send- ja Sync-traitit, emme tarvitse toteuttaa näitä traiteja manuaalisesti. Merkkaustraiteina niillä ei ole edes toteutettavia metodeja. Ne ovat hyödyllisiä vain rinnakkaisuuteen liittyvien invarianttien pakottamiseen.

Näiden traitien manuaalinen toteuttaminen edellyttää turvattoman Rust-koodin kirjoittamista. Käsittelemme turvattoman Rust-koodin käyttöä luvussa 20; toistaiseksi tärkeää tietää on, että uusien rinnakkaisuustyyppien rakentaminen, jotka eivät koostu Send- ja Sync-osista, vaatii huolellista pohdintaa turvallisuustakuujen ylläpitämiseksi. ”The Rustonomicon” sisältää lisätietoa näistä takuista ja niiden ylläpidosta.

Yhteenveto

Tämä ei ole viimeinen kerta, kun kohtaamme rinnakkaisuuden tässä kirjassa: seuraava luku keskittyy asynkroniseen ohjelmointiin, ja luvun 21 projekti käyttää tämän luvun käsitteitä realistisemmassa tilanteessa kuin tässä käsitellyt pienemmät esimerkit.

Kuten aiemmin mainittiin, koska hyvin vähän siitä, miten Rust käsittelee rinnakkaisuutta, on osa kieltä, monet rinnakkaisuusratkaisut toteutetaan kirjastoina. Nämä kehittyvät nopeammin kuin standardikirjasto, joten kannattaa etsiä verkosta ajankohtaisia, huippuluokan kirjastoja monisäikeisiin tilanteisiin.

Rustin standardikirjasto tarjoaa kanavia viestinvälitykseen ja älykkäitä osoitintyyppejä, kuten Mutex<T> ja Arc<T>, jotka ovat turvallisia käyttää rinnakkaisissa konteksteissa. Tyyppijärjestelmä ja lainanvalvonta varmistavat, että näitä ratkaisuja käyttävä koodi ei päädy tietokilpailutilanteisiin tai virheellisiin viittauksiin. Kun saat koodisi kääntymään, voit olla varma, että se toimii useilla säikeillä ilman muiden kielten yleisiä vaikeasti jäljitettäviä vikoja. Rinnakkaisohjelmointi ei ole enää käsite, jota kannattaa pelätä: eteenpäin ja tee ohjelmistasi rinnakkaisia – pelottomasti!

Asynkronisen ohjelmoinnin perusteet: Async, Await, Futures ja Streams

Monet tietokoneelle antamamme tehtävät voivat kestää kauan. Olisi mukavaa, jos voisimme tehdä jotain muuta samalla, kun odotamme näiden pitkien prosessien valmistumista. Modernit tietokoneet tarjoavat kaksi tekniikkaa useamman kuin yhden toiminnon työstämiseen samanaikaisesti: rinnakkaisuuden ja samanaikaisuuden. Ohjelmien logiikka on kuitenkin kirjoitettu pääosin lineaarisesti. Haluaisimme pystyä määrittelemään, mitä operaatioita ohjelman tulisi suorittaa ja missä kohdissa funktio voisi pysähtyä ja jokin ohjelman toinen osa voisi sen sijaan suorittua, ilman että meidän tarvitsee etukäteen määritellä tarkasti jokaisen koodinpätkän suoritusjärjestystä ja -tapaa. Asynkroninen ohjelmointi on abstraktio, jonka avulla voimme ilmaista koodimme mahdollisina pysäytyskohtina ja lopullisina tuloksina, ja joka hoitaa koordinoinnin yksityiskohdat puolestamme.

Tämä luku rakentuu luvun 16 säikeiden käytön päälle rinnakkaisuudessa ja samanaikaisuudessa esittelemällä vaihtoehtoisen tavan koodin kirjoittamiseen: Rustin futuret, streamit sekä async- ja await-syntaksi, joiden avulla voimme ilmaista, miten operaatiot voivat olla asynkronisia, ja kolmannen osapuolen crate:t, jotka toteuttavat asynkroniset ajoympäristöt: koodia, joka hallinnoi ja koordinoi asynkronisten operaatioiden suoritusta.

Tarkastellaan esimerkkiä. Oletetaan, että viet perhejuhlasta tekemääsi videota, mikä voi kestää minuuteista tunteihin. Videon vienti käyttää niin paljon CPU- ja GPU-tehoa kuin se voi. Jos sinulla olisi vain yksi CPU-ydin ja käyttöjärjestelmäsi ei keskeyttäisi vientiä ennen kuin se valmistuu — eli jos se suorittaisi viennin synkronisesti — et voisi tehdä mitään muuta tietokoneellasi, kun tehtävä on käynnissä. Se olisi varsin turhauttava kokemus. Onneksi tietokoneesi käyttöjärjestelmä voi — ja tekee — keskeyttää viennin tarpeeksi usein näkymättömästi, jotta voit tehdä muuta työtä samanaikaisesti.

Oletetaan sitten, että lataat jonkun toisen jakamaa videota, mikä voi myös kestää kauan, mutta ei vie yhtä paljon CPU-aikaa. Tässä tapauksessa CPU:n täytyy odottaa, että data saapuu verkosta. Vaikka voit alkaa lukea dataa heti, kun se alkaa saapua, voi kestää jonkin aikaa, ennen kuin kaikki on perillä. Vaikka data olisi kokonaan paikalla, suuren videon lataaminen voi kestää ainakin sekunnin tai kaksi. Se ei ehkä kuulosta paljolta, mutta modernille prosessorille se on hyvin pitkä aika: se voi suorittaa miljardeja operaatioita sekunnissa. Taas käyttöjärjestelmäsi keskeyttää ohjelmasi näkymättömästi, jotta CPU voi tehdä muuta työtä odottaessaan verkkokutsun valmistumista.

Videon vienti on esimerkki CPU-rajoitteisesta tai laskentarajoitteisesta operaatiosta. Se on rajoitettu tietokoneen mahdollisella datankäsittelynopeudella CPU:ssa tai GPU:ssa ja sillä, kuinka suuren osan tästä nopeudesta operaatio voi käyttää. Videon lataus on esimerkki I/O-rajoitteisesta operaatiosta, koska se on rajoitettu tietokoneen syötteen ja tulosteen nopeudella; se voi edetä vain niin nopeasti kuin data voidaan lähettää verkon yli.

Molemmissa esimerkeissä käyttöjärjestelmän näkymättömät keskeytykset tarjoavat eräänlaisen samanaikaisuuden. Tämä samanaikaisuus tapahtuu kuitenkin vain koko ohjelman tasolla: käyttöjärjestelmä keskeyttää yhden ohjelman, jotta muut ohjelmat voivat tehdä työtä. Monissa tapauksissa, koska ymmärrämme ohjelmiamme paljon tarkemmin kuin käyttöjärjestelmä, voimme havaita samanaikaisuuden mahdollisuuksia, joita käyttöjärjestelmä ei näe.

Esimerkiksi jos rakennamme työkalua tiedostolatausten hallintaan, meidän pitäisi pystyä kirjoittamaan ohjelma niin, että yhden latauksen aloittaminen ei lukitse käyttöliittymää, ja käyttäjien pitäisi pystyä aloittamaan useita latauksia samanaikaisesti. Monet käyttöjärjestelmän verkko-API:t ovat kuitenkin estäviä; eli ne estävät ohjelman etenemisen, kunnes käsiteltävä data on täysin valmiina.

Huom: Tämä on tapa, jolla useimmat funktiokutsut toimivat, jos asiaa miettii. Termiä estävä (blocking) käytetään kuitenkin yleensä tiedostoihin, verkkoon tai tietokoneen muihin resursseihin liittyvistä funktiokutsuista, koska näissä tapauksissa yksittäinen ohjelma hyötyisi siitä, että operaatio olisi ei-estävä.

Voisimme välttää pääsäikeen estämisen luomalla erillisen säikeen jokaisen tiedoston lataamiseen. Näiden säikeiden käyttämien järjestelmäresurssien yläraja muodostuisi kuitenkin lopulta ongelmaksi. Olisi parempi, jos kutsu ei estäisi alun perinkään, vaan voisimme määritellä joukon tehtäviä, jotka haluamme ohjelman suorittavan, ja antaa ajoympäristön valita parhaan järjestyksen ja tavan niiden suorittamiseen.

Juuri tämän Rustin async-abstraktio (lyhenne sanasta asynchronous, eli asynkroninen) tarjoaa. Tässä luvussa opit kaiken asyncista seuraavien aiheiden kautta:

  • Kuinka käyttää Rustin async- ja await-syntaksia ja suorittaa asynkronisia funktioita ajoympäristössä
  • Kuinka käyttää async-mallia ratkaisemaan joitain samoja haasteita, joita tarkastelimme luvussa 16
  • Kuinka monisäikeisyys ja async tarjoavat toisiaan täydentäviä ratkaisuja, joita voi monissa tapauksissa yhdistellä

Ennen kuin näemme, miten async toimii käytännössä, meidän täytyy kuitenkin tehdä lyhyt poikkeus ja käsitellä rinnakkaisuuden ja samanaikaisuuden erot.

Rinnakkaisuus ja samanaikaisuus

Olemme tähän mennessä käsitelleet rinnakkaisuutta ja samanaikaisuutta lähes synonyymeinä. Nyt meidän täytyy erottaa ne tarkemmin, koska erot tulevat esiin, kun alamme työskennellä.

Ajatellaan eri tapoja, joilla tiimi voi jakaa työn ohjelmistoprojektissa. Voit antaa yhdelle jäsenelle useita tehtäviä, antaa jokaiselle jäsenelle yhden tehtävän tai käyttää näiden kahden lähestymistavan yhdistelmää.

Kun yksittäinen henkilö työskentelee usean eri tehtävän parissa ennen kuin yksikään niistä on valmis, kyseessä on samanaikaisuus. Yksi tapa toteuttaa samanaikaisuus on samankaltainen kuin kaksi eri projektia checkoutattuna tietokoneellesi: kun kyllästyt tai jäät jumiin yhdessä projektissa, vaihdat toiseen. Olet vain yksi ihminen, joten et voi edistää molempia tehtäviä täsmälleen samalla hetkellä, mutta voit moniajaa ja edistää yhtä kerrallaan vaihtamalla niiden välillä (katso kuva 17-1).

A diagram with stacked boxes labeled Task A and Task B, with diamonds in them representing subtasks. Arrows point from A1 to B1, B1 to A2, A2 to B2, B2 to A3, A3 to A4, and A4 to B3. The arrows between the subtasks cross the boxes between Task A and Task B.
Kuva 17-1: Samanaikainen työnkulku, jossa vaihdetaan tehtävän A ja tehtävän B välillä

Kun tiimi jakaa tehtäväryhmän niin, että jokainen jäsen ottaa yhden tehtävän ja työskentelee sen parissa yksin, kyseessä on rinnakkaisuus. Jokainen tiimin jäsen voi edistyä täsmälleen samanaikaisesti (katso kuva 17-2).

A diagram with stacked boxes labeled Task A and Task B, with diamonds in them representing subtasks. Arrows point from A1 to A2, A2 to A3, A3 to A4, B1 to B2, and B2 to B3. No arrows cross between the boxes for Task A and Task B.
Kuva 17-2: Rinnakkainen työnkulku, jossa tehtävän A ja tehtävän B työ etenee itsenäisesti

Molemmissa työnkuluissa saatat joutua koordinoimaan eri tehtävien välillä. Ehkä luulit, että yhdelle henkilölle annettu tehtävä on täysin riippumaton muiden työstä, mutta se vaatiikin toisen tiimin jäsenen tehtävän valmistumisen ensin. Osa työstä voitiin tehdä rinnakkain, mutta osa oli itse asiassa sarjallista: se voitiin tehdä vain sarjana, yksi tehtävä toisen jälkeen, kuten kuvassa 17-3.

A diagram with stacked boxes labeled Task A and Task B, with diamonds in them representing subtasks. In Task A, arrows point from A1 to A2, from A2 to a pair of thick vertical lines like a “pause” symbol, and from that symbol to A3. In task B, arrows point from B1 to B2, from B2 to B3, from B3 to A3, and from B3 to B4.
Kuva 17-3: Osittain rinnakkainen työnkulku, jossa tehtävän A ja tehtävän B työ etenee itsenäisesti, kunnes tehtävä A3 on estynyt tehtävän B3 tulosten takia.

Vastaavasti saatat huomata, että yksi omista tehtävistäsi riippuu toisesta omasta tehtävästäsi. Nyt samanaikainen työsi on myös muuttunut sarjalliseksi.

Rinnakkaisuus ja samanaikaisuus voivat myös leikata toisiinsa. Jos huomaat, että kollega on jumissa, kunnes saat yhden tehtävistäsi valmiiksi, keskityt todennäköisesti kaikki ponnistelusi siihen tehtävään ”vapauttaaksesi” kollegasi. Sinä ja työkaverisi ette enää voi työskennellä rinnakkain, ettekä myöskään samanaikaisesti omien tehtävienne parissa.

Sama perusdynamiikka tulee esiin ohjelmistoissa ja laitteistossa. Koneella, jossa on yksi CPU-ydin, CPU voi suorittaa vain yhden operaation kerrallaan, mutta se voi silti työskennellä samanaikaisesti. Työkaluja kuten säikeitä, prosesseja ja asyncia käyttäen tietokone voi keskeyttää yhden toiminnon ja vaihtaa muihin ennen kuin se palaa lopulta takaisin ensimmäiseen. Koneella, jossa on useita CPU-ytimiä, se voi myös tehdä työtä rinnakkain. Yksi ydin voi suorittaa yhtä tehtävää, kun toinen ydin suorittaa täysin erillistä tehtävää, ja nämä operaatiot tapahtuvat todella samanaikaisesti.

Async-koodin suorittaminen Rustissa tapahtuu yleensä samanaikaisesti. Riippuen laitteistosta, käyttöjärjestelmästä ja käyttämästämme async-ajoympäristöstä (ajoympäristöistä puhumme pian lisää), tämä samanaikaisuus voi myös hyödyntää rinnakkaisuutta taustalla.

Nyt sukellamme siihen, miten async-ohjelmointi Rustissa oikeasti toimii.

Futures ja async-syntaksi

Futuret ja async-syntaksi

Asynkronisen ohjelmoinnin keskeiset elementit Rustissa ovat futuret sekä Rustin async- ja await-avainsanat.

future on arvo, joka ei ehkä ole valmis nyt, mutta tulee valmiiksi jossain vaiheessa tulevaisuudessa. (Sama käsite esiintyy monissa kielissä, joskus muilla nimillä kuten task tai promise.) Rust tarjoaa Future-traitin rakennuspalikkana, jotta eri async-operaatiot voidaan toteuttaa eri tietorakenteilla mutta yhteisellä rajapinnalla. Rustissa futuret ovat tyyppejä, jotka toteuttavat Future-traitin. Jokainen future sisältää omat tietonsa edistymisestä ja siitä, mitä ”valmis” tarkoittaa.

Voit käyttää async-avainsanaa lohkoissa ja funktioissa määrittääksesi, että ne voidaan keskeyttää ja jatkaa. Async-lohkon tai async-funktion sisällä voit käyttää await-avainsanaa odottaaksesi futurea (eli odottaaksesi, että se tulee valmiiksi). Jokainen kohta, jossa odotat futurea async-lohkon tai -funktion sisällä, on mahdollinen paikka, jossa kyseinen lohko tai funktio voi pysähtyä ja jatkaa. Prosessia, jossa tarkistetaan futurelta, onko sen arvo jo saatavilla, kutsutaan pollaukseksi.

Joissakin muissa kielissä, kuten C#:ssa ja JavaScriptissä, käytetään myös async- ja await-avainsanoja async-ohjelmointiin. Jos tunnet nämä kielet, saatat huomata merkittäviä eroja siinä, miten Rust käsittelee syntaksia. Siihen on hyvä syy, kuten näemme!

Kun kirjoitamme async-Rustia, käytämme async- ja await-avainsanoja useimman aikaa. Rust kääntää ne vastaavaksi koodiksi, joka käyttää Future-traitiä, aivan kuten se kääntää for-silmukat vastaavaksi koodiksi, joka käyttää Iterator-traitiä. Koska Rust tarjoaa Future-traitin, voit kuitenkin myös toteuttaa sen omille tietotyypeillesi tarvittaessa. Monet tämän luvun funktiot palauttavat tyyppejä, joilla on oma Future-toteutuksensa. Palaamme traitin määrittelyyn luvun lopussa ja syvennymme siihen, miten se toimii, mutta tämä riittää pitämään meidät liikkeessä.

Tämä voi tuntua abstraktilta, joten kirjoitetaan ensimmäinen async-ohjelmamme: pieni verkkoskreipperi. Välitämme komentoriviltä kaksi URL-osoitetta, haemme molemmat samanaikaisesti ja palautamme sen tuloksen, joka valmistuu ensin. Esimerkissä on melko paljon uutta syntaksia, mutta älä huoli — selitämme kaiken tarvittavan matkan varrella.

Ensimmäinen async-ohjelmamme

Jotta voimme keskittyä tässä luvussa asyncin oppimiseen eikä ekosysteemin osien jongleeraukseen, olemme luoneet trpl-crate:n (trpl on lyhenne sanasta ”The Rust Programming Language”). Se uudelleen-vie kaikki tarvitsemasi tyypit, traitit ja funktiot, pääasiassa futures- ja tokio -crate:istä. futures-crate on virallinen kokeilualusta Rustin async-koodille, ja se on itse asiassa paikka, jossa Future-trait alun perin suunniteltiin. Tokio on tänään laajimmin käytetty async-ajoympäristö Rustissa, erityisesti web-sovelluksissa. Muita hyviä ajoympäristöjä on olemassa, ja ne voivat sopia paremmin tarkoituksiisi. Käytämme tokio-cratea trpl:n alla, koska se on hyvin testattu ja laajalti käytetty.

Joissakin tapauksissa trpl myös nimeää uudelleen tai käärii alkuperäisiä API:ja, jotta voit keskittyä tämän luvun oleellisiin yksityiskohtiin. Jos haluat ymmärtää, mitä crate tekee, kannustamme tutustumaan sen lähdekoodiin. Näet, mistä crate:stä kukin uudelleenvienti tulee, ja olemme jättäneet laajat kommentit selittämään crate:n toimintaa.

Luo uusi binääriprojekti nimeltä hello-async ja lisää trpl-crate riippuvuudeksi:

$ cargo new hello-async
$ cd hello-async
$ cargo add trpl

Nyt voimme käyttää trpl:n tarjoamia osia ensimmäisen async-ohjelmamme kirjoittamiseen. Rakennamme pienen komentorivityökalun, joka hakee kaksi verkkosivua, poimii kustakin <title>-elementin ja tulostaa sen sivun otsikon, joka valmistuu koko prosessin ensimmäisenä.

page_title-funktion määrittely

Aloitetaan kirjoittamalla funktio, joka ottaa yhden sivun URL-osoitteen parametrina, tekee siihen pyynnön ja palauttaa <title>-elementin tekstin (katso listaus 17-1).

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-01/src/main.rs:all}}
}
Listing 17-1: Async-funktion määrittely HTML-sivun title-elementin hakemiseksi

Ensin määrittelemme funktion nimeltä page_title ja merkitsemme sen async-avainsanalla. Sitten käytämme trpl::get-funktiota välitetyn URL-osoitteen hakemiseen ja lisäämme await-avainsanan odottamaan vastausta. Saadaksemme response:n tekstin kutsumme sen text-metodia ja odotamme sitä jälleen await-avainsanalla. Molemmat vaiheet ovat asynkronisia. get-funktiossa meidän täytyy odottaa, että palvelin lähettää vastauksensa ensimmäisen osan, joka sisältää HTTP-otsikot, evästeet ja niin edelleen ja joka voidaan toimittaa erillään vastauksen rungosta. Erityisesti jos runko on hyvin suuri, kaiken saapuminen voi kestää jonkin aikaa. Koska meidän täytyy odottaa vastauksen kokonaisuuden saapumista, text-metodi on myös async.

Meidän täytyy odottaa molempia futureja eksplisiittisesti, koska futuret Rustissa ovat laiskoja: ne eivät tee mitään, ennen kuin pyydät niitä await-avainsanalla. (Itse asiassa Rust näyttää kääntäjävaroituksen, jos et käytä futurea.) Tämä saattaa muistuttaa iteraattorien käsittelyä ”Kohteiden sarjan käsittely iteraattoreilla” -osiossa luvussa 13. Iteraattorit eivät tee mitään, ellet kutsu niiden next-metodia — suoraan tai for-silmukoiden tai map-kaltaisten metodien kautta, jotka käyttävät next:iä taustalla. Samoin futuret eivät tee mitään, ellet pyydä niitä eksplisiittisesti. Tämä laiskuus antaa Rustin välttää async-koodin suorittamisen, kunnes sitä todella tarvitaan.

Huom: Tämä eroaa käyttäytymisestä, jonka näimme käytettäessä thread::spawn:ia ”Uuden säikeen luominen spawn:illa” -osiossa luvussa 16, jossa toiselle säikeelle välittämämme sulkeuma alkoi suorittua heti. Se eroaa myös monien muiden kielten async-lähestymistavasta. Se on kuitenkin tärkeää, jotta Rust voi tarjota suorituskykytakuunsa, aivan kuten iteraattoreiden kanssa.

Kun meillä on response_text, voimme jäsentää sen Html-tyypin instanssiksi käyttämällä Html::parse:a. Raakamerkkijonon sijaan meillä on nyt tietotyyppi, jolla voimme käsitellä HTML:ää rikkaampana tietorakenteena. Erityisesti voimme käyttää select_first-metodia löytääksemme annetun CSS-valitsimen ensimmäisen esiintymän. Välittämällä merkkijonon "title" saamme dokumentin ensimmäisen <title>-elementin, jos sellainen on. Koska vastaavaa elementtiä ei välttämättä ole, select_first palauttaa Option<ElementRef>:n. Lopuksi käytämme Option::map-metodia, jonka avulla voimme käsitellä Option:in sisältämää kohdetta, jos se on läsnä, ja olla tekemättä mitään, jos sitä ei ole. (Voisimme käyttää myös match-lauseketta, mutta map on idiomaattisempi.) map:ille antamamme funktion rungossa kutsumme inner_html:ia title:lle saadaksemme sen sisällön, joka on String. Lopulta meillä on Option<String>.

Huomaa, että Rustin await-avainsana tulee odotettavan lausekkeen jälkeen, ei ennen sitä. Se on siis jälkiliite-avainsana. Tämä voi erota siitä, mihin olet tottunut, jos olet käyttänyt async:ia muissa kielissä, mutta Rustissa se tekee metodiketjuista paljon miellyttävämpiä. Näin voimme muuttaa page_title:n rungon ketjuttamaan trpl::get- ja text-funktiokutsut yhteen await:in välissä, kuten listauksessa 17-2.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-02/src/main.rs:chaining}}
}
Listing 17-2: Ketjuttaminen await-avainsanalla

Näin olemme onnistuneesti kirjoittaneet ensimmäisen async-funktiomme! Ennen kuin lisäämme main:iin koodia sen kutsumiseksi, puhutaan hieman lisää siitä, mitä olemme kirjoittaneet ja mitä se tarkoittaa.

Kun Rust näkee async-avainsanalla merkityn lohkon, se kääntää sen yksilölliseksi, nimettömäksi tietotyypiksi, joka toteuttaa Future-traitin. Kun Rust näkee async-avainsanalla merkityn funktion, se kääntää sen ei-async-funktioksi, jonka runko on async-lohko. Async-funktion palautustyyppi on kääntäjän kyseiselle async-lohkolle luoman nimettömän tietotyypin tyyppi.

Näin ollen async fn:n kirjoittaminen on vastaavaa kuin funktion kirjoittaminen, joka palauttaa palautustyypin futuren. Kääntäjälle funktiomäärittely, kuten listauksen 17-1 async fn page_title, on karkeasti vastaava ei-async-funktiolle, joka on määritelty näin:

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}
}

Käydään läpi muunnetun version jokainen osa:

  • Se käyttää impl Trait -syntaksia, josta puhuimme luvussa 10 ”Traitit parametreina” -osiossa.
  • Palautettu arvo toteuttaa Future-traitin, jonka assosioitu tyyppi on Output. Huomaa, että Output-tyyppi on Option<String>, sama kuin alkuperäisen async fn page_title -version palautustyyppi.
  • Kaikki alkuperäisen funktion rungossa kutsuttu koodi on kääritty async move -lohkoon. Muista, että lohkot ovat lausekkeita. Koko lohko on funktion palauttama lauseke.
  • Tämä async-lohko tuottaa arvon tyypillä Option<String>, kuten juuri kuvattiin. Arvo vastaa palautustyypin Output-tyyppiä. Tämä on sama kuin muut näkemäsi lohkot.
  • Uusi funktion runko on async move -lohko, koska se käyttää url-parametria. (Puhumme async:sta ja async move:sta paljon lisää myöhemmin luvussa.)

Nyt voimme kutsua page_title:a main:issa.

Async-funktion suorittaminen ajoympäristössä

Aloitetaan hakemalla yhden sivun otsikko, kuten listauksessa 17-3. Valitettavasti tämä koodi ei vielä käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-03/src/main.rs:main}}
Listing 17-3: page_title-funktion kutsuminen main:ista käyttäjän antamalla argumentilla

Noudatamme samaa mallia, jota käytimme komentoriviargumenttien lukemiseen ”Komentoriviargumenttien hyväksyminen” -osiossa luvussa 12. Sitten välitämme URL-argumentin page_title:lle ja odotamme tulosta. Koska futuren tuottama arvo on Option<String>, käytämme match-lauseketta tulostaaksemme eri viestejä sen mukaan, oliko sivulla <title>.

await-avainsanaa voi käyttää vain async-funktioissa tai -lohkoissa, eikä Rust anna merkitä erityistä main-funktiota async:iksi.

error[E0752]: `main` function is not allowed to be `async`
 --> src/main.rs:6:1
  |
6 | async fn main() {
  | ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

Syy siihen, miksi main:ia ei voi merkitä async:iksi, on se, että async-koodi tarvitsee ajoympäristön: Rust-crate:n, joka hallinnoi asynkronisen koodin suorittamisen yksityiskohdat. Ohjelman main-funktio voi alustaa ajoympäristön, mutta se ei itse ole ajoympäristö. (Näemme pian lisää siitä, miksi näin on.) Jokaisessa Rust-ohjelmassa, joka suorittaa async-koodia, on vähintään yksi paikka, jossa ajoympäristö asetetaan suorittamaan futuret.

Useimmat asyncia tukevat kielet sisällyttävät ajoympäristön, mutta Rust ei. Sen sijaan saatavilla on monia eri async-ajoympäristöjä, joista jokainen tekee erilaisia kompromisseja käyttötapaansa sopiviksi. Esimerkiksi suuren läpimenon web-palvelimella, jossa on monta CPU-ydintä ja paljon RAM-muistia, on hyvin erilaiset tarpeet kuin mikrokontrollerilla, jossa on yksi ydin, vähän RAM-muistia eikä heap-allokointimahdollisuutta. Nämä ajoympäristöt tarjoavat usein myös async-versioita yleisestä toiminnallisuudesta, kuten tiedosto- tai verkko-I/O:sta.

Tässä ja luvun lopun osissa käytämme trpl-crate:n block_on-funktiota, joka ottaa futuren argumenttina ja estää nykyisen säikeen, kunnes tämä future on suoritettu loppuun. Taustalla block_on:in kutsuminen asettaa tokio-crate:lla ajoympäristön, jota käytetään välitetyn futuren suorittamiseen (trpl-crate:n block_on:in käyttäytyminen on samankaltainen kuin muiden ajoympäristöcrate:jen block_on-funktioilla). Kun future on valmis, block_on palauttaa futuren tuottaman arvon.

Voisimme välittää page_title:n palauttaman futuren suoraan block_on:ille ja, kun se valmistuu, tehdä match:in tuloksena olevaan Option<String>:iin kuten yritimme listauksessa 17-3. Useimmissa luvun esimerkeissä (ja useimmassa oikean maailman async-koodissa) teemme kuitenkin enemmän kuin yhden async-funktiokutsun, joten sen sijaan välitämme async-lohkon ja odotamme eksplisiittisesti page_title-kutsun tulosta, kuten listauksessa 17-4.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-04/src/main.rs:run}}
Listing 17-4: Async-lohkon odottaminen trpl::block_on:illa

Kun suoritamme tämän koodin, saamme alun perin odottamamme käyttäytymisen:

$ cargo run -- "https://www.rust-lang.org"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
            Rust Programming Language

Huh — meillä on vihdoin toimivaa async-koodia! Ennen kuin lisäämme koodin kahden sivun kilpailuttamiseen, käännymme hetkeksi takaisin siihen, miten futuret toimivat.

Jokainen odotuspiste — eli jokainen paikka, jossa koodi käyttää await-avainsanaa — on paikka, jossa ohjaus palautetaan ajoympäristölle. Jotta tämä toimii, Rustin täytyy pitää kirjaa async-lohkoon liittyvästä tilasta, jotta ajoympäristö voisi käynnistää muuta työtä ja palata sitten, kun se on valmis yrittämään ensimmäisen etenemistä uudelleen. Tämä on näkymätön tilakone, ikään kuin olisit kirjoittanut enumin, joka tallentaa nykyisen tilan jokaisessa odotuspisteessä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/no-listing-state-machine/src/lib.rs:enum}}
}

Jokaisen tilan välillä siirtymisen koodin kirjoittaminen käsin olisi kuitenkin työlästä ja virhealtista, erityisesti kun myöhemmin täytyy lisätä toiminnallisuutta ja tiloja. Onneksi Rust-kääntäjä luo ja hallinnoi async-koodin tilakonedatarakenteet automaattisesti. Normaalit lainaus- ja omistussäännöt datarakenteille pätevät edelleen, ja onneksi kääntäjä tarkistaa nekin puolestamme ja antaa hyödyllisiä virheilmoituksia. Käymme läpi muutamia niistä myöhemmin luvussa.

Lopulta jonkun täytyy suorittaa tämä tilakone, ja se jokin on ajoympäristö. (Siksi saatat törmätä executor-mainintoihin tutkiessasi ajoympäristöjä: executor on ajoympäristön osa, joka vastaa async-koodin suorittamisesta.)

Nyt näet, miksi kääntäjä esti meitä tekemästä main:ista itse async-funktiota listauksessa 17-3. Jos main olisi async-funktio, jonkun muun täytyisi hallita main:in palauttaman futuren tilakonetta, mutta main on ohjelman lähtökohta! Sen sijaan kutsuimme trpl::block_on-funktiota main:issa asettaaksemme ajoympäristön ja suorittaaksemme async-lohkon palauttaman futuren loppuun.

Huom: Jotkut ajoympäristöt tarjoavat makroja, joiden avulla voit kirjoittaa async-main-funktion. Nämä makrot kirjoittavat async fn main() { ... } uudelleen tavalliseksi fn main:iksi, joka tekee saman kuin teimme käsin listauksessa 17-4: kutsuu funktiota, joka suorittaa futuren loppuun samalla tavalla kuin trpl::block_on.

Nyt yhdistetään nämä palaset ja katsotaan, miten voimme kirjoittaa samanaikaista koodia.

Kahden URL-osoitteen kilpailuttaminen samanaikaisesti

Listauksessa 17-5 kutsumme page_title:a kahdella eri komentoriviltä välitetyllä URL-osoitteella ja kilpailutamme ne valitsemalla sen futuren, joka valmistuu ensin.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-05/src/main.rs:all}}
Listing 17-5: page_title:n kutsuminen kahdelle URL-osoitteelle nähdäksemme, kumpi palautuu ensin

Aloitamme kutsumalla page_title:a kummallekin käyttäjän antamalle URL-osoitteelle. Tallennamme tuloksena olevat futuret nimillä title_fut_1 ja title_fut_2. Muista, että ne eivät vielä tee mitään, koska futuret ovat laiskoja emmekä ole vielä odottaneet niitä. Sitten välitämme futuret trpl::select:ille, joka palauttaa arvon ilmaisemaan, kumpi sille välitetyistä futureista valmistuu ensin.

Huom: Taustalla trpl::select on rakennettu yleisemmän select-funktion päälle, joka on määritelty futures-crate:ssä. futures-crate:n select-funktio voi tehdä paljon asioita, joita trpl::select ei voi, mutta siinä on myös lisämonimutkaisuutta, jonka voimme ohittaa toistaiseksi.

Kumpikin future voi oikeutetusti ”voittaa”, joten Result:in palauttaminen ei ole järkevää. Sen sijaan trpl::select palauttaa tyypin, jota emme ole vielä nähneet: trpl::Either. Either-tyyppi on hieman samankaltainen kuin Result, sillä siinä on kaksi tapausta. Toisin kuin Result:issa, Either:iin ei ole sisäänrakennettua käsitettä onnistumisesta tai epäonnistumisesta. Sen sijaan se käyttää Left:ia ja Right:ia ilmaisemaan ”jompaa kumpaa”:

#![allow(unused)]
fn main() {
enum Either<A, B> {
    Left(A),
    Right(B),
}
}

select-funktio palauttaa Left:in kyseisen futuren tuloksella, jos ensimmäinen argumentti voittaa, ja Right:in toisen future-argumentin tuloksella, jos se voittaa. Tämä vastaa argumenttien järjestystä funktiokutsussa: ensimmäinen argumentti on toisen vasemmalla puolella.

Päivitämme myös page_title:n palauttamaan saman välitetyn URL-osoitteen. Näin, jos ensin palautuva sivu ei sisällä ratkaistavaa <title>:ä, voimme silti tulostaa järkevän viestin. Kun tämä tieto on käytettävissä, viimeistelemme päivittämällä println!-tulostuksemme ilmaisemaan sekä kumpi URL-osoite valmistui ensin että mikä, jos lainkaan, kyseisen URL-osoitteen verkkosivun <title> on.

Olet nyt rakentanut pienen toimivan verkkoskreipperin! Valitse pari URL-osoitetta ja suorita komentorivityökalu. Saatat huomata, että jotkin sivustot ovat johdonmukaisesti nopeampia kuin toiset, kun taas toisissa tapauksissa nopeampi sivu vaihtelee ajosta toiseen. Tärkeämpää on, että olet oppinut futurejen kanssa työskentelyn perusteet, joten voimme nyt syventyä siihen, mitä asyncilla voi tehdä.

Rinnakkaisuuden soveltaminen async:lla

Samanaikaisuuden soveltaminen asyncilla

Tässä osiossa sovellamme asyncia joihinkin samoihin samanaikaisuushaasteisiin, joita käsittelimme säikeillä luvussa 16. Koska käsittelimme siellä jo monia keskeisiä ideoita, keskitymme tässä osiossa siihen, mikä eroaa säikeiden ja futurejen välillä.

Monissa tapauksissa asyncilla työskentelyn API:t ovat hyvin samankaltaisia kuin säikeillä työskentelyn API:t. Toisissa tapauksissa ne ovat hyvin erilaisia. Vaikka API:t näyttäisivät samankaltaisilta säikeiden ja asyncin välillä, niillä on usein erilainen käyttäytyminen — ja niillä on lähes aina erilaiset suorituskykyominaisuudet.

Uuden tehtävän luominen spawn_task:illa

Ensimmäinen operaatio, jota käsittelimme ”Uuden säikeen luominen spawn:illa” -osiossa luvussa 16, oli laskeminen kahdella erillisellä säikeellä. Tehdään sama asyncilla. trpl-crate tarjoaa spawn_task-funktion, joka näyttää hyvin samankaltaiselta kuin thread::spawn-API, ja sleep-funktion, joka on async-versio thread::sleep-API:sta. Voimme käyttää näitä yhdessä laskuesimerkin toteuttamiseen, kuten listauksessa 17-6.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-06/src/main.rs:all}}
}
Listing 17-6: Uuden tehtävän luominen tulostamaan yhtä asiaa, kun päätehtävä tulostaa jotain muuta

Lähtökohtanamme asetamme main-funktion trpl::block_on:illa, jotta ylätason funktiomme voi olla async.

Huom: Tästä eteenpäin luvussa jokainen esimerkki sisältää täsmälleen saman käärintäkoodin trpl::block_on:illa main:issa, joten ohitamme sen usein samalla tavalla kuin main:in. Muista sisällyttää se koodiisi!

Sitten kirjoitamme kaksi silmukkaa kyseisen lohkon sisään, joista kummassakin on trpl::sleep-kutsu, joka odottaa puoli sekuntia (500 millisekuntia) ennen seuraavan viestin lähettämistä. Sijoitamme yhden silmukan trpl::spawn_task:n runkoon ja toisen ylätason for-silmukkaan. Lisäämme myös await:in sleep-kutsujen jälkeen.

Tämä koodi käyttäytyy samankaltaisesti kuin säikeisiin perustuva toteutus — mukaan lukien se, että saatat nähdä viestien ilmestyvän eri järjestyksessä omassa terminaalissasi, kun suoritat sen:

hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!

Tämä versio pysähtyy heti, kun pää-async-lohkon rungossa oleva for-silmukka päättyy, koska spawn_task:n luoma tehtävä sammutetaan, kun main-funktio päättyy. Jos haluat sen suorittuvan aina tehtävän valmistumiseen asti, tarvitset join-kahvan odottamaan ensimmäisen tehtävän valmistumista. Säikeillä käytimme join-metodia ”estääksemme” säikeen valmistumiseen asti. Listauksessa 17-7 voimme käyttää await:ia samaan tarkoitukseen, koska tehtäväkahva itsessään on future. Sen Output-tyyppi on Result, joten puramme sen myös await:in jälkeen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-07/src/main.rs:handle}}
}
Listing 17-7: await:in käyttö join-kahvan kanssa tehtävän suorittamiseksi loppuun

Tämä päivitetty versio suorittaa, kunnes molemmat silmukat päättyvät:

hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!

Tähän mennessä näyttää siltä, että async ja säikeet antavat samankaltaiset tulokset, vain eri syntaksilla: await:in käyttö join-kutsun sijaan join-kahvalla ja sleep-kutsujen odottaminen.

Suurempi ero on se, että emme tarvinneet luoda toista käyttöjärjestelmän säiettä tähän. Itse asiassa emme edes tarvitse luoda tehtävää tässä. Koska async-lohkot käännetään nimettömiksi futureiksi, voimme sijoittaa kummankin silmukan async-lohkoon ja antaa ajoympäristön suorittaa molemmat loppuun trpl::join-funktiolla.

”Kaikkien säikeiden valmistumisen odottaminen” -osiossa luvussa 16 näytimme, miten join-metodia käytetään std::thread::spawn:in palauttaman JoinHandle-tyypin kanssa. trpl::join-funktio on samankaltainen, mutta futureille. Kun annat sille kaksi futurea, se tuottaa yhden uuden futuren, jonka tulos on monikko kummankin välittämäsi futuren tuloksista, kun ne molemmat ovat valmiita. Näin listauksessa 17-8 käytämme trpl::join:ia odottamaan sekä fut1:n että fut2:n valmistumista. Emme odota fut1:tä ja fut2:tä, vaan trpl::join:n tuottamaa uutta futurea. Jätämme tuloksen huomiotta, koska se on vain monikko, joka sisältää kaksi yksikköarvoa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-08/src/main.rs:join}}
}
Listing 17-8: trpl::join:in käyttö kahden nimettömän futuren odottamiseen

Kun suoritamme tämän, näemme molempien futurejen suorittuvan loppuun:

hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!

Nyt näet täsmälleen saman järjestyksen joka kerta, mikä on hyvin erilaista kuin säikeillä ja trpl::spawn_task:illa listauksessa 17-7. Tämä johtuu siitä, että trpl::join-funktio on reilu: se tarkistaa kummankin futuren yhtä usein, vuorotellen niitä, eikä koskaan anna toisen edetä, jos toinen on valmis. Säikeillä käyttöjärjestelmä päättää, mitä säiettä tarkistaa ja kuinka kauan antaa sen suorittaa. Async-Rustissa ajoympäristö päättää, mitä tehtävää tarkistaa. (Käytännössä yksityiskohdat monimutkaistuvat, koska async-ajoympäristö voi käyttää käyttöjärjestelmän säikeitä taustalla osana samanaikaisuuden hallintaa, joten reiluuden takaaminen voi olla ajoympäristölle enemmän työtä — mutta se on silti mahdollista!) Ajoympäristöjen ei tarvitse taata reiluutta millekään operaatiolle, ja ne tarjoavat usein eri API:ja, joiden avulla voit valita, haluatko reiluutta.

Kokeile joitakin näistä variaatioista futurejen odottamisessa ja katso, mitä ne tekevät:

  • Poista async-lohko kummankin tai molempien silmukoiden ympäriltä.
  • Odota kumpaakin async-lohkoa heti sen määrittelyn jälkeen.
  • Kääri vain ensimmäinen silmukka async-lohkoon ja odota tuloksena olevaa futurea toisen silmukan rungon jälkeen.

Lisähaasteena katso, osaatko päätellä, mikä tuloste on kussakin tapauksessa ennen koodin suorittamista!

Datan lähettäminen kahden tehtävän välillä viestinvälityksellä

Datan jakaminen futurejen välillä on myös tuttua: käytämme jälleen viestinvälitystä, mutta tällä kertaa async-versioita tyypeistä ja funktioista. Kuljemme hieman eri polkua kuin ”Datan siirtäminen säikeiden välillä viestinvälityksellä” -osiossa luvussa 16 havainnollistaaksemme keskeisiä eroja säikeisiin ja futureihin perustuvan samanaikaisuuden välillä. Listauksessa 17-9 aloitamme vain yhdellä async-lohkolla — emme luo erillistä tehtävää kuten loimme erillisen säikeen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-09/src/main.rs:channel}}
}
Listing 17-9: Async-kanavan luominen ja kahden puoliskon antaminen tx:lle ja rx:lle

Tässä käytämme trpl::channel:ia, async-version monituottaja-yksittäiskuluttaja-kanava-API:sta, jota käytimme säikeillä luvussa 16. Async-versio API:sta eroaa vain vähän säikeisiin perustuvasta versiosta: se käyttää muuttuvaa eikä muuttumatonta vastaanottajaa rx:ää, ja sen recv-metodi tuottaa futuren, jota meidän täytyy odottaa, sen sijaan että se tuottaisi arvon suoraan. Nyt voimme lähettää viestejä lähettäjältä vastaanottajalle. Huomaa, että emme tarvitse luoda erillistä säiettä tai edes tehtävää; meidän täytyy vain odottaa rx.recv-kutsua.

Synkroninen Receiver::recv-metodi std::mpsc::channel:issa estää, kunnes se vastaanottaa viestin. trpl::Receiver::recv-metodi ei estä, koska se on async. Sen sijaan että estäisi, se palauttaa ohjauksen ajoympäristölle, kunnes viesti vastaanotetaan tai kanavan lähetyspuoli sulkeutuu. Sitä vastoin emme odota send-kutsua, koska se ei estä. Sen ei tarvitse, koska kanava, johon lähetämme, on rajoittamaton.

Huom: Koska kaikki tämä async-koodi suoritetaan async-lohkossa trpl::block_on-kutsussa, kaikki sen sisällä voi välttää estämisen. Koodi sen ulkopuolella kuitenkin estyy, kunnes block_on-funktio palaa. Siinä on koko trpl::block_on-funktion idea: sen avulla voit valita, missä estät jonkin async-koodin joukon, ja siten missä siirryt synkronisen ja asynkronisen koodin välillä.

Huomaa tästä esimerkistä kaksi asiaa. Ensinnäkin viesti saapuu heti. Toiseksi, vaikka käytämme tässä futurea, samanaikaisuutta ei vielä ole. Kaikki listauksessa tapahtuu peräkkäin, aivan kuten ilman futureja.

Käsitellään ensimmäinen osa lähettämällä sarja viestejä ja nukkumalla niiden välissä, kuten listauksessa 17-10.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-10/src/main.rs:many-messages}}
Listing 17-10: Useiden viestien lähettäminen ja vastaanottaminen async-kanavan yli ja nukkuminen await:illa jokaisen viestin välissä

Viestien lähettämisen lisäksi meidän täytyy vastaanottaa ne. Tässä tapauksessa, koska tiedämme kuinka monta viestiä on tulossa, voisimme tehdä sen käsin kutsumalla rx.recv().await neljä kertaa. Oikeassa maailmassa odotamme kuitenkin yleensä tuntemattoman määrän viestejä, joten meidän täytyy odottaa, kunnes päätämme, ettei viestejä enää tule.

Listauksessa 16-10 käytimme for-silmukkaa käsittelemään kaikki synkronisesta kanavasta vastaanotetut kohteet. Rustilla ei kuitenkaan ole vielä tapaa käyttää for-silmukkaa asynkronisesti tuotetun kohteiden sarjan kanssa, joten meidän täytyy käyttää silmukkaa, jota emme ole vielä nähneet: while let -ehdosilmukkaa. Tämä on silmukkaversio if let -rakenteesta, jonka näimme ”Tiivis ohjausvirta if let:illä ja let...else:llä” -osiossa luvussa 6. Silmukka jatkaa suorittamista niin kauan kuin sen määrittelemä kuvio vastaa edelleen arvoa.

rx.recv-kutsu tuottaa futuren, jota odotamme. Ajoympäristö keskeyttää futuren, kunnes se on valmis. Kun viesti saapuu, future ratkeaa Some(message)-arvoksi niin monta kertaa kuin viestejä saapuu. Kun kanava sulkeutuu — riippumatta siitä, ovatko mitkään viestit saapuneet — future ratkeaa sen sijaan None:ksi ilmaisemaan, ettei arvoja ole enempää ja että meidän pitäisi lopettaa pollaus — eli lopettaa odottaminen.

while let -silmukka yhdistää kaiken tämän. Jos rx.recv().await:in tulos on Some(message), saamme käyttöön viestin ja voimme käyttää sitä silmukan rungossa, aivan kuten if let:illä. Jos tulos on None, silmukka päättyy. Joka kerta kun silmukka suoritetaan loppuun, se osuu odotuspisteeseen uudelleen, joten ajoympäristö keskeyttää sen uudelleen, kunnes toinen viesti saapuu.

Koodi lähettää ja vastaanottaa nyt onnistuneesti kaikki viestit. Valitettavasti on vielä pari ongelmaa. Ensinnäkin viestit eivät saavu puolen sekunnin välein. Ne saapuvat kaikki kerralla 2 sekunnin (2 000 millisekunnin) kuluttua ohjelman käynnistymisestä. Toiseksi tämä ohjelma ei myöskään koskaan päätty! Sen sijaan se odottaa ikuisesti uusia viestejä. Sinun täytyy sammuttaa se painamalla ctrl-C.

Yhden async-lohkon sisällä oleva koodi suoritetaan lineaarisesti

Aloitetaan tutkimalla, miksi viestit saapuvat kaikki kerralla koko viiveen jälkeen sen sijaan, että ne saapuisivat viivein välein. Tietyssä async-lohkossa await-avainsanojen esiintymisjärjestys koodissa on myös järjestys, jossa ne suoritetaan ohjelman käydessä.

Listauksessa 17-10 on vain yksi async-lohko, joten kaikki sen sisällä suoritetaan lineaarisesti. Samanaikaisuutta ei vieläkään ole. Kaikki tx.send-kutsut tapahtuvat vuorotellen kaikkien trpl::sleep-kutsujen ja niihin liittyvien odotuspisteiden kanssa. Vasta sitten while let -silmukka pääsee käymään läpi mitään recv-kutsujen odotuspisteitä.

Saadaksemme haluamamme käyttäytymisen, jossa nukkumisviive tapahtuu jokaisen viestin välissä, meidän täytyy sijoittaa tx- ja rx-operaatiot omiin async-lohkoihinsa, kuten listauksessa 17-11. Sitten ajoympäristö voi suorittaa kummankin erikseen käyttämällä trpl::join:ia, aivan kuten listauksessa 17-8. Taas odotamme trpl::join:in kutsumisen tulosta, emme yksittäisiä futureja. Jos odottaisimme yksittäisiä futureja peräkkäin, päätyisimme takaisin peräkkäiseen kulkuun — juuri sitä, mitä yritämme välttää.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-11/src/main.rs:futures}}
Listing 17-11: send- ja recv-operaatioiden erottaminen omiin async-lohkoihinsa ja näiden lohkojen futurejen odottaminen

Listauksen 17-11 päivitetyllä koodilla viestit tulostetaan 500 millisekunnin välein sen sijaan, että ne tulostuisivat kaikki kerralla 2 sekunnin kuluttua.

Omistajuuden siirtäminen async-lohkoon

Ohjelma ei kuitenkaan vieläkään päätty, koska while let -silmukan ja trpl::join:in vuorovaikutus:

  • trpl::join:in palauttama future valmistuu vasta, kun molemmat sille välitetyt futuret ovat valmiita.
  • tx_fut-future valmistuu, kun se on nukkunut viimeisen viestin lähettämisen jälkeen vals:issa.
  • rx_fut-future ei valmistu, ennen kuin while let -silmukka päättyy.
  • while let -silmukka ei päätty, ennen kuin rx.recv:n odottaminen tuottaa None:n.
  • rx.recv:n odottaminen palauttaa None:n vain, kun kanavan toinen pää on suljettu.
  • Kanava sulkeutuu vain, jos kutsumme rx.close:a tai kun lähettäjäpuoli tx pudotetaan.
  • Emme kutsu rx.close:a missään, eikä tx pudotu ennen kuin trpl::block_on:ille välitetty ulompi async-lohko päättyy.
  • Lohko ei voi päättyä, koska se on estynyt trpl::join:in valmistumiseen, mikä vie meidät takaisin tämän listan alkuun.

Tällä hetkellä viestejä lähettävä async-lohko vain lainaa tx:ää, koska viestin lähettäminen ei vaadi omistajuutta, mutta jos voisimme siirtää tx:n kyseiseen async-lohkoon, se pudotettaisiin, kun lohko päättyy. ”Viittausten kaappaaminen tai omistajuuden siirtäminen” -osiossa luvussa 13 opit käyttämään move-avainsanaa sulkeumien kanssa, ja kuten käsiteltiin move-sulkeumien käyttö säikeiden kanssa” -osiossa luvussa 16, meidän täytyy usein siirtää data sulkeumiin säikeillä työskennellessä. Sama perusdynamiikka pätee async-lohkoihin, joten move-avainsana toimii async-lohkojen kanssa samalla tavalla kuin sulkeumien kanssa.

Listauksessa 17-12 muutamme viestien lähettämiseen käytetyn lohkon muodosta async muotoon async move.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-12/src/main.rs:with-move}}
}
Listing 17-12: Listauksen 17-11 koodin versio, joka sammuttuu oikein valmistuttuaan

Kun suoritamme tämän version koodista, se sammuttuu siististi viimeisen viestin lähettämisen ja vastaanottamisen jälkeen. Seuraavaksi katsotaan, mitä pitäisi muuttaa lähettääksemme dataa useammasta kuin yhdestä futuresta.

Usean futuren yhdistäminen join!-makrolla

Tämä async-kanava on myös monituottaja-kanava, joten voimme kutsua clone:a tx:lle, jos haluamme lähettää viestejä useista futureista, kuten listauksessa 17-13.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-13/src/main.rs:here}}
}
Listing 17-13: Usean tuottajan käyttö async-lohkojen kanssa

Ensin kloonaamme tx:n luoden tx1:n ensimmäisen async-lohkon ulkopuolelle. Siirrämme tx1:n kyseiseen lohkoon kuten aiemmin tx:n kanssa. Sitten myöhemmin siirrämme alkuperäisen tx:n uuteen async-lohkoon, jossa lähetämme lisää viestejä hieman hitaammalla viiveellä. Sijoitamme tämän uuden async-lohkon vastaanottavaan async-lohkoon jälkeen, mutta se voisi olla yhtä hyvin ennen sitä. Ratkaisevaa on järjestys, jossa futureja odotetaan, ei järjestys, jossa ne luodaan.

Molempien viestejä lähettävien async-lohkojen täytyy olla async move -lohkoja, jotta sekä tx että tx1 pudotetaan, kun lohkot päättyvät. Muuten päädymme takaisin samaan loputtomaan silmukkaan, jossa aloitimme.

Lopuksi vaihdamme trpl::join:ista trpl::join!:iin käsitelläksemme lisäfuturen: join!-makro odottaa mielivaltaisen määrän futureja, joiden määrä tiedetään kääntöaikana. Käsittelemme tuntemattoman määrän futureja myöhemmin tässä luvussa.

Nyt näemme kaikki viestit molemmista lähettävistä futureista, ja koska lähettävät futuret käyttävät hieman erilaisia viiveitä lähettämisen jälkeen, viestit vastaanotetaan myös näillä eri väleillä:

received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'

Olemme tutkineet, miten viestinvälitystä käytetään datan lähettämiseen futurejen välillä, miten async-lohkon sisällä oleva koodi suoritetaan peräkkäin, miten omistajuus siirretään async-lohkoon ja miten useita futureja yhdistetään. Seuraavaksi keskustellaan siitä, miten ja miksi kerrotaan ajoympäristölle, että se voi vaihtaa toiseen tehtävään.

Työskentely minkä tahansa määrän futuresien kanssa

Ohjauksen luovuttaminen ajoympäristölle

Muista ”Ensimmäinen async-ohjelmamme” -osiosta, että jokaisessa odotuspisteessä Rust antaa ajoympäristölle mahdollisuuden keskeyttää tehtävä ja vaihtaa toiseen, jos odotettava future ei ole valmis. Päinvastoin pätee myös: Rust keskeyttää async-lohkot ja palauttaa ohjauksen ajoympäristölle vain odotuspisteessä. Kaikki odotuspisteiden välillä on synkronista.

Tämä tarkoittaa, että jos teet paljon työtä async-lohkossa ilman odotuspistettä, kyseinen future estää muita futureja etenemästä. Tätä kutsutaan joskus tilanteeksi, jossa yksi future nälkiinnyttää muita futureja. Joissakin tapauksissa se ei ole suuri ongelma. Jos kuitenkin teet jonkinlaista kallista alustusta tai pitkään kestävää työtä, tai jos sinulla on future, joka jatkaa tiettyä tehtävää loputtomasti, sinun täytyy miettiä, milloin ja missä luovutat ohjauksen takaisin ajoympäristölle.

Simuloidaan pitkään kestävää operaatiota havainnollistaaksemme nälkiinnytysongelmaa ja tutkitaan sitten, miten se ratkaistaan. Listaus 17-14 esittelee slow-funktion.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-14/src/main.rs:slow}}
}
Listing 17-14: thread::sleep:in käyttö hitaiden operaatioiden simulointiin

Tämä koodi käyttää std::thread::sleep:ia trpl::sleep:in sijaan, joten slow:in kutsuminen estää nykyisen säikeen tietyn määrän millisekunteja. Voimme käyttää slow:ia todellisten maailman operaatioiden sijaisena, jotka ovat sekä pitkään kestäviä että estäviä.

Listauksessa 17-15 käytämme slow:ia emuloimaan tämänkaltaisen CPU-rajoitteisen työn tekemistä parissa futurea.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-15/src/main.rs:slow-futures}}
}
Listing 17-15: slow-funktion kutsuminen hitaiden operaatioiden simulointiin

Kumpikin future palauttaa ohjauksen ajoympäristölle vasta sen jälkeen, kun se on suorittanut joukon hitaita operaatioita. Jos suoritat tämän koodin, näet tämän tulosteen:

'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.

Kuten listauksessa 17-5, jossa kilpailutimme kahta URL-osoitetta hakevia futureja trpl::select:illa, select päättyy edelleen heti, kun a on valmis. slow-kutsujen välillä ei kuitenkaan ole vuorottelua kahden futuren välillä. a-future tekee kaiken työnsä, kunnes trpl::sleep-kutsua odotetaan, sitten b-future tekee kaiken työnsä, kunnes sen oma trpl::sleep-kutsu odotetaan, ja lopuksi a-future valmistuu. Jotta molemmat futuret voisivat edetä hitaiden tehtäviensä välillä, tarvitsemme odotuspisteitä, jotta voimme luovuttaa ohjauksen takaisin ajoympäristölle. Tarvitsemme siis jotain, mitä voimme odottaa!

Näemme tämänkaltaisen luovutuksen jo listauksessa 17-15: jos poistaisimme a-futuren lopun trpl::sleep:in, se valmistuisi ilman, että b-future suorittuisi lainkaan. Kokeillaan käyttää trpl::sleep-funktiota lähtökohtana operaatioiden vuorottelulle etenemisessä, kuten listauksessa 17-16.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-16/src/main.rs:here}}
}
Listing 17-16: trpl::sleep:in käyttö operaatioiden vuorottelun sallimiseksi etenemisessä

Olemme lisänneet trpl::sleep-kutsuja odotuspisteillä jokaisen slow-kutsun väliin. Nyt kahden futuren työ on vuoroteltua:

'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.

a-future suorittaa edelleen hetken ennen ohjauksen luovuttamista b:lle, koska se kutsuu slow:ia ennen kuin koskaan kutsuu trpl::sleep:ia, mutta sen jälkeen futuret vaihtavat vuorotellen aina, kun jompikumpi osuu odotuspisteeseen. Tässä tapauksessa teimme sen jokaisen slow-kutsun jälkeen, mutta voimme jakaa työn millä tavalla meille sopii.

Emme kuitenkaan todella halua nukkua tässä: haluamme edetä niin nopeasti kuin voimme. Meidän täytyy vain luovuttaa ohjaus takaisin ajoympäristölle. Voimme tehdä sen suoraan käyttämällä trpl::yield_now-funktiota. Listauksessa 17-17 korvaamme kaikki trpl::sleep-kutsut trpl::yield_now:illa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-17/src/main.rs:yields}}
}
Listing 17-17: yield_now:in käyttö operaatioiden vuorottelun sallimiseksi etenemisessä

Tämä koodi on sekä selkeämpi todellisesta tarkoituksesta että voi olla merkittävästi nopeampi kuin sleep:in käyttö, koska sleep:in käyttämien ajastimien tarkkuudella on usein rajoja. Käyttämämme sleep-versio nukkuu aina vähintään millisekunnin, vaikka välittäisimme sille yhden nanosekunnin Duration:in. Modernit tietokoneet ovat nopeita: ne voivat tehdä paljon yhdessä millisekunnissa!

Tämä tarkoittaa, että async voi olla hyödyllinen jopa laskentarajoitteisille tehtäville, riippuen siitä, mitä muuta ohjelmasi tekee, koska se tarjoaa hyödyllisen työkalun ohjelman eri osien välisten suhteiden jäsentämiseen (mutta async-tilakoneen yläkulun hinnalla). Tämä on eräänlaista yhteistoiminnallista moniajoa, jossa jokaisella futurella on valta päättää, milloin se luovuttaa ohjauksen odotuspisteiden kautta. Jokaisella futurella on siten myös vastuu välttää liian pitkää estämistä. Joissakin Rust-pohjaisissa sulautetuissa käyttöjärjestelmissä tämä on ainoa moniajon laji!

Oikeassa koodissa et yleensä vuorottele funktiokutsuja ja odotuspisteitä jokaisella rivillä, tietenkään. Vaikka ohjauksen luovuttaminen tällä tavalla on suhteellisen edullista, se ei ole ilmaista. Monissa tapauksissa laskentarajoitteisen tehtävän pilkkominen voi hidastaa sitä merkittävästi, joten joskus on parempi kokonaissuorituskyvyn kannalta antaa operaation estää hetkeksi. Mittaa aina nähdäksesi, missä koodisi todelliset suorituskykypullonkaulat ovat. Taustalla oleva dynamiikka on kuitenkin tärkeä pitää mielessä, jos näet paljon työtä tapahtuvan peräkkäin, kun odotit sen tapahtuvan samanaikaisesti!

Omien async-abstraktioiden rakentaminen

Voimme myös yhdistellä futureja luodaksemme uusia malleja. Esimerkiksi voimme rakentaa timeout-funktion async-rakennuspalikoilla, joita meillä jo on. Kun olemme valmiita, tulos on toinen rakennuspalikka, jota voisimme käyttää luomaan vielä lisää async-abstraktioita.

Listaus 17-18 näyttää, miten odotamme tämän timeout:in toimivan hitaan futuren kanssa.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-18/src/main.rs:here}}
Listing 17-18: Kuvitellun timeout:imme käyttö hitaan operaation suorittamiseen aikarajalla

Toteutetaan tämä! Aloitetaan miettimällä timeout:in API:ta:

  • Sen täytyy olla itse async-funktio, jotta voimme odottaa sitä.
  • Sen ensimmäisen parametrin pitäisi olla suoritettava future. Voimme tehdä siitä geneerisen, jotta se toimii minkä tahansa futuren kanssa.
  • Sen toinen parametri on enimmäisodotusaika. Jos käytämme Duration:ia, sen on helppo välittää eteenpäin trpl::sleep:ille.
  • Sen pitäisi palauttaa Result. Jos future valmistuu onnistuneesti, Result on Ok futuren tuottamalla arvolla. Jos aikaraja umpeutuu ensin, Result on Err odotetulla kestolla.

Listaus 17-19 näyttää tämän määrittelyn.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-19/src/main.rs:declaration}}
Listing 17-19: timeout:in signatuurin määrittely

Tämä täyttää tyyppitavoitteemme. Mietitään nyt tarvittavaa käyttäytymistä: haluamme kilpailuttaa välitetyn futuren keston kanssa. Voimme käyttää trpl::sleep:ia luodaksemme ajastinfuturen kestosta ja trpl::select:ia suorittaaksemme tämän ajastimen kutsujan välittämän futuren kanssa.

Listauksessa 17-20 toteutamme timeout:in tekemällä match:in trpl::select:in odottamisen tulokseen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-20/src/main.rs:implementation}}
}
Listing 17-20: timeout:in määrittely select:illä ja sleep:illä

trpl::select:in toteutus ei ole reilu: se pollaa argumentteja aina siinä järjestyksessä, jossa ne välitetään (muut select-toteutukset valitsevat satunnaisesti, mitä argumenttia pollataan ensin). Välitämme siis future_to_try:n select:ille ensin, jotta sillä on mahdollisuus valmistua, vaikka max_time olisi hyvin lyhyt kesto. Jos future_to_try valmistuu ensin, select palauttaa Left:in future_to_try:n tuloksella. Jos timer valmistuu ensin, select palauttaa Right:in ajastimen ()-tuloksella.

Jos future_to_try onnistuu ja saamme Left(output):in, palautamme Ok(output):in. Jos sleep-ajastin umpeutuu sen sijaan ja saamme Right(()):in, jätämme ():n huomiotta _:llä ja palautamme sen sijaan Err(max_time):in.

Näin meillä on toimiva timeout, joka on rakennettu kahdesta muusta async-apurista. Jos suoritamme koodimme, se tulostaa epäonnistumistilan aikarajan jälkeen:

Failed after 2 seconds

Koska futuret yhdistyvät muihin futureihin, voit rakentaa todella tehokkaita työkaluja pienemmistä async-rakennuspalikoista. Esimerkiksi voit käyttää samaa lähestymistapaa yhdistääksesi aikarajoja uudelleenyrityksiin ja käyttää niitä vuorostaan verkko-operaatioihin (kuten listauksessa 17-5).

Käytännössä työskentelet yleensä suoraan async:in ja await:in kanssa ja toissijaisesti funktioiden kuten select:in ja makrojen kuten join!:n kanssa hallitaksesi, miten uloimmat futuret suoritetaan.

Olemme nyt nähneet useita tapoja työskennellä usean futuren kanssa samanaikaisesti. Seuraavaksi katsomme, miten voimme työskennellä usean futuren kanssa peräkkäin ajan kuluessa streamien avulla.

Streams: Futures peräkkäin

Streamit: futuret peräkkäin

Muista, miten käytimme async-kanavamme vastaanottajaa aiemmin tässä luvussa ”Viestinvälitys” -osiossa. Async-recv-metodi tuottaa kohteiden sarjan ajan kuluessa. Tämä on esimerkki paljon yleisemmästä mallista, jota kutsutaan streamiksi. Monet käsitteet voidaan luontevasti esittää streaminä: jonoon tulevat kohteet, tiedostojärjestelmästä inkrementaalisesti haetut dataosuudet, kun koko aineisto on liian suuri tietokoneen muistille, tai verkossa ajan kuluessa saapuva data. Koska streamit ovat futureja, voimme käyttää niitä minkä tahansa muun futuren kanssa ja yhdistellä niitä mielenkiintoisilla tavoilla. Esimerkiksi voimme niputtaa tapahtumia välttääksemme liian monta verkkokutsua, asettaa aikarajoja pitkään kestävien operaatioiden sarjoille tai rajoittaa käyttöliittymätapahtumia turhan työn välttämiseksi.

Näimme kohteiden sarjan luvussa 13, kun tarkastelimme Iterator-traitiä ”Iterator-trait ja next-metodi” -osiossa, mutta iteraattorien ja async-kanavan vastaanottajan välillä on kaksi eroa. Ensimmäinen ero on aika: iteraattorit ovat synkronisia, kun taas kanavan vastaanottaja on asynkroninen. Toinen ero on API. Kun työskentelemme suoraan Iterator:in kanssa, kutsumme sen synkronista next-metodia. trpl::Receiver-streamin kanssa kutsuimme sen sijaan asynkronista recv-metodia. Muuten nämä API:t tuntuvat hyvin samankaltaisilta, eikä tämä samankaltaisuus ole sattumaa. Stream on kuin asynkroninen iteraation muoto. Vaikka trpl::Receiver odottaa erityisesti viestien vastaanottamista, yleiskäyttöinen stream-API on paljon laajempi: se tarjoaa seuraavan kohteen samalla tavalla kuin Iterator, mutta asynkronisesti.

Iteraattorien ja streamien samankaltaisuus Rustissa tarkoittaa, että voimme itse asiassa luoda streamin mistä tahansa iteraattorista. Kuten iteraattorin kanssa, voimme työskennellä streamin kanssa kutsumalla sen next-metodia ja odottamalla tulosta, kuten listauksessa 17-21, joka ei vielä käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-21/src/main.rs:stream}}
Listing 17-21: Streamin luominen iteraattorista ja sen arvojen tulostaminen

Aloitamme numerotaulukosta, jonka muunnamme iteraattoriksi ja jonka päällä kutsumme map:ia kaikkien arvojen kaksinkertaistamiseksi. Sitten muunnamme iteraattorin streamiksi trpl::stream_from_iter-funktiolla. Seuraavaksi käymme läpi streamin kohteita niiden saapuessa while let -silmukalla.

Valitettavasti kun yritämme suorittaa koodin, se ei käänny vaan ilmoittaa, ettei next-metodia ole saatavilla:

error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
  --> src/main.rs:10:40
   |
10 |         while let Some(value) = stream.next().await {
   |                                        ^^^^
   |
   = help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
   |
1  + use crate::trpl::StreamExt;
   |
1  + use futures_util::stream::stream::StreamExt;
   |
1  + use std::iter::Iterator;
   |
1  + use std::str::pattern::Searcher;
   |
help: there is a method `try_next` with a similar name
   |
10 |         while let Some(value) = stream.try_next().await {
   |                                        ~~~~~~~~

Kuten tämä tuloste selittää, kääntäjävirheen syy on se, että tarvitsemme oikean traitin näkyviin voidaksemme käyttää next-metodia. Keskustelumme perusteella saatat odottaa traitin olevan Stream, mutta se on itse asiassa StreamExt. Lyhenne sanasta extension (laajennus), Ext on yleinen malli Rust-yhteisössä laajentaa yhtä traitiä toisella.

Stream-trait määrittelee matalan tason rajapinnan, joka yhdistää käytännössä Iterator- ja Future-traitit. StreamExt tarjoaa korkeamman tason API-joukon Stream:in päälle, mukaan lukien next-metodin ja muita apumetodeja, jotka ovat samankaltaisia kuin Iterator-traitin tarjoamat. Stream ja StreamExt eivät ole vielä osa Rustin standardikirjastoa, mutta useimmat ekosysteemin crate:t käyttävät samankaltaisia määrittelyjä.

Kääntäjävirheen korjaus on lisätä use-lause trpl::StreamExt:ille, kuten listauksessa 17-22.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-22/src/main.rs:all}}
}
Listing 17-22: Iteraattorin käyttö onnistuneesti streamin pohjana

Kun kaikki nämä palaset on yhdistetty, koodi toimii haluamallamme tavalla! Lisäksi, koska meillä on nyt StreamExt näkyvissä, voimme käyttää kaikkia sen apumetodeja, aivan kuten iteraattoreiden kanssa.

Lähempi tarkastelu async-traitteihin

Async-traitien tarkempi tarkastelu

Olemme tämän luvun ajan käyttäneet Future-, Stream- ja StreamExt-traitiä eri tavoin. Toistaiseksi olemme kuitenkin välttäneet syventymästä liikaa siihen, miten ne toimivat tai miten ne liittyvät toisiinsa, mikä on useimman aikaa ihan hyvä päivittäisessä Rust-työssä. Joskus kohtaat kuitenkin tilanteita, joissa sinun täytyy ymmärtää hieman enemmän näiden traitien yksityiskohtia sekä Pin-tyyppiä ja Unpin-traitiä. Tässä osiossa syvennymme juuri tarpeeksi auttamaan näissä tilanteissa, jättäen todella syvän sukelluksen muulle dokumentaatiolle.

Future-trait

Aloitetaan tarkastelemalla tarkemmin, miten Future-trait toimii. Näin Rust määrittelee sen:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Tämä trait-määrittely sisältää joukon uusia tyyppejä ja myös syntaksia, jota emme ole vielä nähneet, joten käydään määrittely läpi pala kerrallaan.

Ensinnäkin Future:n assosioitu tyyppi Output kertoo, mihin future ratkeaa. Tämä on analoginen Iterator-traitin Item-assosioitulle tyypille. Toiseksi Future:lla on poll-metodi, joka ottaa self-parametrilleen erityisen Pin-viittauksen ja muuttuvan viittauksen Context-tyyppiin ja palauttaa Poll<Self::Output>:in. Puhumme Pin:stä ja Context:ista hetken kuluttua. Keskitytään toistaiseksi siihen, mitä metodi palauttaa, Poll-tyyppiin:

#![allow(unused)]
fn main() {
pub enum Poll<T> {
    Ready(T),
    Pending,
}
}

Tämä Poll-tyyppi on samankaltainen kuin Option. Siinä on yksi variantti, jossa on arvo, Ready(T), ja yksi ilman arvoa, Pending. Poll tarkoittaa kuitenkin jotain hyvin erilaista kuin Option! Pending-variantti ilmaisee, että futurella on vielä työtä tekemättä, joten kutsujan täytyy tarkistaa uudelleen myöhemmin. Ready-variantti ilmaisee, että Future on saanut työnsä valmiiksi ja T-arvo on saatavilla.

Huom: poll:ia on harvoin tarpeen kutsua suoraan, mutta jos joudut tekemään sen, muista, että useimmissa futureissa kutsujan ei pitäisi kutsua poll:ia uudelleen sen jälkeen, kun future on palauttanut Ready:n. Monet futuret panikoivat, jos niitä pollataan uudelleen valmistuttuaan. Futuret, joita on turvallista pollata uudelleen, sanovat sen eksplisiittisesti dokumentaatiossaan. Tämä on samankaltaista kuin Iterator::next:n käyttäytyminen.

Kun näet koodia, joka käyttää await:ia, Rust kääntää sen taustalla koodiksi, joka kutsuu poll:ia. Jos palaat listaukseen 17-4, jossa tulostimme yhden URL-osoitteen sivun otsikon sen ratkettua, Rust kääntää sen joksikin (vaikkakaan ei täsmälleen) tämän kaltaiseksi:

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
    Pending => {
        // But what goes here?
    }
}

Mitä meidän pitäisi tehdä, kun future on vielä Pending? Tarvitsemme tavan yrittää uudelleen, ja uudelleen, ja uudelleen, kunnes future vihdoin on valmis. Toisin sanoen tarvitsemme silmukan:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll() {
        Ready(value) => match page_title {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
        Pending => {
            // continue
        }
    }
}

Jos Rust kääntäisi sen täsmälleen tähän koodiin, jokainen await estäisi — juuri päinvastoin kuin tavoittelimme! Sen sijaan Rust varmistaa, että silmukka voi luovuttaa ohjauksen jollekin, joka voi keskeyttää työn tämän futuren parissa tehdäkseen työtä muilla futureilla ja tarkistaa tämän myöhemmin uudelleen. Kuten olemme nähneet, tuo jokin on async-ajoympäristö, ja tämä aikataulutus ja koordinointi on yksi sen päätehtävistä.

”Datan lähettäminen kahden tehtävän välillä viestinvälityksellä” -osiossa kuvasimme rx.recv:n odottamista. recv-kutsu palauttaa futuren, ja futuren odottaminen pollaa sitä. Huomautimme, että ajoympäristö keskeyttää futuren, kunnes se on valmis joko Some(message):lla tai None:lla, kun kanava sulkeutuu. Syvemmällä ymmärryksellä Future-traitistä ja erityisesti Future::poll:ista näemme, miten se toimii. Ajoympäristö tietää, ettei future ole valmis, kun se palauttaa Poll::Pending:in. Vastaavasti ajoympäristö tietää, että future on valmis ja edistää sitä, kun poll palauttaa Poll::Ready(Some(message)):n tai Poll::Ready(None):n.

Tarkat yksityiskohdat siitä, miten ajoympäristö tekee tämän, ovat tämän kirjan laajuuden ulkopuolella, mutta keskeistä on nähdä futurejen perusmekaniikka: ajoympäristö pollaa jokaista vastuullaan olevaa futurea ja asettaa futuren takaisin lepotilaan, kun se ei ole vielä valmis.

Pin-tyyppi ja Unpin-trait

Listauksessa 17-13 käytimme trpl::join!-makroa odottamaan kolmea futurea. On kuitenkin yleistä, että kokoelma — kuten vektori — sisältää jonkin määrän futureja, joiden määrää ei tiedetä ennen ajoa. Muutetaan listaus 17-13 listauksen 17-23 koodiksi, joka sijoittaa kolme futurea vektoriin ja kutsuu trpl::join_all-funktiota sen sijaan, mikä ei vielä käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch17-async-await/listing-17-23/src/main.rs:here}}
Listing 17-23: Futurejen odottaminen kokoelmassa

Sijoitamme jokaisen futuren Box:iin tehdäksemme niistä trait-objekteja, aivan kuten teimme luvun 12 ”Virheiden palauttaminen run:ista” -osiossa. (Käsittelemme trait-objektit yksityiskohtaisesti luvussa 18.) Trait-objektien käyttö antaa meidän käsitellä kunkin näiden tyyppien tuottamaa nimettömää futurea samana tyyppinä, koska kaikki ne toteuttavat Future-traitin.

Tämä saattaa yllättää. Loppujen lopuksi mikään async-lohko ei palauta mitään, joten jokainen tuottaa Future<Output = ()>:n. Muista kuitenkin, että Future on trait ja että kääntäjä luo yksilöllisen enumin jokaiselle async-lohkolle, vaikka niillä olisi identtiset tulostyypit. Aivan kuten et voi laittaa kahta eri käsin kirjoitettua structia Vec:iin, et voi sekoittaa kääntäjän luomia enumeja.

Sitten välitämme futurejen kokoelman trpl::join_all-funktiolle ja odotamme tulosta. Tämä ei kuitenkaan käänny; tässä on virheilmoitusten olennainen osa.

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).await;
   |                                 ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
   = note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

Tämän virheilmoituksen huomautus kertoo, että meidän pitäisi käyttää pin!-makroa kiinnittääksemme arvot, eli sijoittaa ne Pin-tyyppiin, joka takaa, ettei arvoja siirretä muistissa. Virheilmoitus sanoo, että kiinnitys vaaditaan, koska dyn Future<Output = ()>:n täytyy toteuttaa Unpin-trait, eikä se tällä hetkellä toteuta sitä.

trpl::join_all-funktio palauttaa structin nimeltä JoinAll. Kyseinen struct on geneerinen tyypin F suhteen, joka on rajoitettu toteuttamaan Future-traitin. Futuren odottaminen suoraan await:illa kiinnittää futuren implisiittisesti. Siksi emme tarvitse pin!:ää kaikkialla, missä haluamme odottaa futureja.

Emme kuitenkaan odota futurea suoraan tässä. Sen sijaan rakennamme uuden futuren, JoinAll:in, välittämällä futurejen kokoelman join_all-funktiolle. join_all:n signatuuri vaatii, että kokoelman kohteiden tyypit kaikki toteuttavat Future-traitin, ja Box<T> toteuttaa Future:n vain, jos sen käärimä T on future, joka toteuttaa Unpin-traitin.

Tämä on paljon sulattavaa! Ymmärtääksemme sen todella, sukellamme hieman syvemmälle siihen, miten Future-trait oikeasti toimii, erityisesti kiinnityksen ympärillä. Katso jälleen Future-traitin määrittely:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // Required method
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

cx-parametri ja sen Context-tyyppi ovat avain siihen, miten ajoympäristö tietää, milloin tarkistaa minkäkin futuren ja pysyy silti laiskana. Taaskin yksityiskohdat ovat tämän luvun laajuuden ulkopuolella, ja yleensä sinun täytyy miettiä tätä vain kirjoittaessasi oman Future-toteutuksen. Keskitymme sen sijaan self:n tyyppiin, koska tämä on ensimmäinen kerta, kun näemme metodin, jossa self:llä on tyyppiannotaatio. self:n tyyppiannotaatio toimii kuten muiden funktioparametrien tyyppiannotaatiot, mutta kahdella keskeisellä erolla:

  • Se kertoo Rustille, millaisen tyypin self:n täytyy olla, jotta metodia voidaan kutsua.
  • Se ei voi olla mikä tahansa tyyppi. Se on rajoitettu siihen tyyppiin, jolle metodi on toteutettu, viittaukseen tai älykkääseen osoittimeen kyseiseen tyyppiin tai Pin:iin, joka käärii viittauksen kyseiseen tyyppiin.

Näemme lisää tästä syntaksista luvussa 18. Toistaiseksi riittää tietää, että jos haluamme pollata futurea tarkistaaksemme, onko se Pending vai Ready(Output), tarvitsemme Pin:illä käärityn muuttuvan viittauksen tyyppiin.

Pin on kääre osoittimia muistuttaville tyypeille kuten &, &mut, Box ja Rc. (Teknisesti Pin toimii tyyppien kanssa, jotka toteuttavat Deref- tai DerefMut-traitit, mutta tämä on käytännössä vastaavaa kuin työskennellä vain viittausten ja älykkäiden osoittimien kanssa.) Pin ei itse ole osoitin eikä sillä ole omaa käyttäytymistä kuten Rc:llä ja Arc:illa viittaustenlaskennassa; se on puhtaasti työkalu, jota kääntäjä voi käyttää rajoittaakseen osoittimien käyttöä.

Muistaen, että await toteutetaan poll-kutsujen kautta, alkaa selittää aiemmin näkemämme virheilmoituksen, mutta se oli Unpin:n termeillä, ei Pin:n. Miten Pin liittyy siis Unpin:iin, ja miksi Future tarvitsee self:n olevan Pin-tyypissä poll:in kutsumiseksi?

Muista tämän luvun alusta, että futuren odotuspisteet käännetään tilakoneeksi ja kääntäjä varmistaa, että tilakone noudattaa Rustin normaaleja turvallisuussääntöjä, mukaan lukien lainauksen ja omistajuuden. Jotta tämä toimii, Rust katsoo, mitä dataa tarvitaan yhden odotuspisteen ja seuraavan odotuspisteen tai async-lohkon lopun välillä. Se luo sitten vastaavan variantin käännettyyn tilakoneeseen. Jokainen variantti saa tarvitsemansa pääsyn dataan, jota käytetään kyseisessä lähdekoodin osassa, joko ottamalla omistuksen datasta tai saamalla muuttuvan tai muuttumattoman viittauksen siihen.

Tähän asti kaikki hyvin: jos teemme jotain väärin omistajuuden tai viittausten suhteen tietyssä async-lohkossa, lainaustarkistin kertoo sen meille. Kun haluamme siirtää kyseistä lohkoa vastaavaa futurea — kuten siirtää sen Vec:iin käytettäväksi join_all:in kanssa tai palauttaa sen funktiosta — asiat monimutkaistuvat.

Kun siirrämme futurea — työntämällä sen tietorakenteeseen käytettäväksi join_all:in kanssa tai palauttamalla sen funktiosta — se tarkoittaa itse asiassa Rustin meille luoman tilakoneen siirtämistä. Ja toisin kuin useimmat muut tyypit Rustissa, async-lohkoille Rustin luomat futuret voivat päätyä viittaamaan itseensä minkä tahansa variantin kentissä, kuten yksinkertaistetussa kuvassa 17-4.

A single-column, three-row table representing a future, fut1, which has data values 0 and 1 in the first two rows and an arrow pointing from the third row back to the second row, representing an internal reference within the future.
Kuva 17-4: Itseensä viittaava tietotyyppi

Oletuksena kuitenkin mikä tahansa objekti, jolla on viittaus itseensä, on turvaton siirtää, koska viittaukset osoittavat aina sen todellisen muistiosoitteen, johon ne viittaavat (katso kuva 17-5). Jos siirrät itse tietorakenteen, nämä sisäiset viittaukset jäävät osoittamaan vanhaa sijaintia. Tuo muistisijainti on kuitenkin nyt virheellinen. Ensinnäkin sen arvoa ei päivitetä, kun teet muutoksia tietorakenteeseen. Toiseksi — ja tärkeämpänä — tietokone saa nyt käyttää tuon muistin uudelleen muihin tarkoituksiin! Saatat lopulta lukea täysin asiaan liittymätöntä dataa.

Two tables, depicting two futures, fut1 and fut2, each of which has one column and three rows, representing the result of having moved a future out of fut1 into fut2. The first, fut1, is grayed out, with a question mark in each index, representing unknown memory. The second, fut2, has 0 and 1 in the first and second rows and an arrow pointing from its third row back to the second row of fut1, representing a pointer that is referencing the old location in memory of the future before it was moved.
Kuva 17-5: Itseensä viittaavan tietotyypin siirtämisen turvaton tulos

Teoriassa Rust-kääntäjä voisi yrittää päivittää jokaisen viittauksen objektiin aina, kun sitä siirretään, mutta se voisi lisätä paljon suorituskykyyn liittyvää yläkulua, erityisesti jos koko viittausten verkko täytyy päivittää. Jos sen sijaan voisimme varmistaa, ettei kyseistä tietorakennetta siirretä muistissa, meidän ei tarvitsisi päivittää viittauksia. Tätä varten Rustin lainaustarkistin on olemassa: turvallisessa koodissa se estää sinua siirtämästä mitään kohdetta, johon on aktiivinen viittaus.

Pin rakentuu tämän päälle antaakseen meille juuri tarvitsemamme takuun. Kun kiinnitämme arvon käärimällä osoittimen kyseiseen arvoon Pin:iin, sitä ei voi enää siirtää. Jos siis sinulla on Pin<Box<SomeType>>, kiinnität itse asiassa SomeType-arvon, ei Box-osoitinta. Kuva 17-6 havainnollistaa tätä prosessia.

Three boxes laid out side by side. The first is labeled “Pin”, the second “b1”, and the third “pinned”. Within “pinned” is a table labeled “fut”, with a single column; it represents a future with cells for each part of the data structure. Its first cell has the value “0”, its second cell has an arrow coming out of it and pointing to the fourth and final cell, which has the value “1” in it, and the third cell has dashed lines and an ellipsis to indicate there may be other parts to the data structure. All together, the “fut” table represents a future which is self-referential. An arrow leaves the box labeled “Pin”, goes through the box labeled “b1” and terminates inside the “pinned” box at the “fut” table.
Kuva 17-6: `Box`:in kiinnittäminen, joka osoittaa itseensä viittaavaan future-tyyppiin

Itse asiassa Box-osoitin voi edelleen liikkua vapaasti. Muista: meidän täytyy varmistaa, että lopulta viitattu data pysyy paikallaan. Jos osoitin liikkuu, mutta sen osoittama data on samassa paikassa, kuten kuvassa 17-7, ongelmaa ei ole. (Itsenäisenä harjoituksena tutustu tyyppien dokumentaatioon sekä std::pin-moduuliin ja yritä selvittää, miten tekisit tämän Pin:illä, joka käärii Box:in.) Keskeistä on, ettei itseensä viittaavaa tyyppiä voi siirtää, koska se on edelleen kiinnitetty.

Four boxes laid out in three rough columns, identical to the previous diagram with a change to the second column. Now there are two boxes in the second column, labeled “b1” and “b2”, “b1” is grayed out, and the arrow from “Pin” goes through “b2” instead of “b1”, indicating that the pointer has moved from “b1” to “b2”, but the data in “pinned” has not moved.
Kuva 17-7: `Box`:in siirtäminen, joka osoittaa itseensä viittaavaan future-tyyppiin

Useimmat tyypit ovat kuitenkin täysin turvallisia siirtää, vaikka ne sattuisivat olemaan Pin-osoittimen takana. Meidän täytyy miettiä kiinnitystä vain, kun kohteilla on sisäisiä viittauksia. Alkeisarvot kuten numerot ja totuusarvot ovat turvallisia, koska niissä ei selvästikään ole sisäisiä viittauksia. Myöskään useimmissa tyypeissä, joita normaalisti käytät Rustissa, ei ole ongelmaa. Voit esimerkiksi siirtää Vec:iä huoletta. Tähänastisen perusteella, jos sinulla on Pin<Vec<String>>, joudut tekemään kaiken Pin:in tarjoamien turvallisten mutta rajoittavien API:en kautta, vaikka Vec<String> on aina turvallinen siirtää, jos siihen ei ole muita viittauksia. Tarvitsemme tavan kertoa kääntäjälle, että on turvallista siirtää kohteita tapauksissa kuten tämä — ja siihen tulee Unpin.

Unpin on merkki-trait, samankaltainen kuin Send- ja Sync-traitit, joita näimme luvussa 16, eikä sillä siis ole omaa toiminnallisuutta. Merkki-traitit ovat olemassa vain kertoakseen kääntäjälle, että tietyn traitin toteuttavan tyypin käyttö tietyssä kontekstissa on turvallista. Unpin kertoo kääntäjälle, että tietyllä tyypillä ei tarvitse ylläpitää takeita siitä, voidaanko kyseinen arvo turvallisesti siirtää.

Aivan kuten Send:n ja Sync:n kanssa, kääntäjä toteuttaa Unpin:in automaattisesti kaikille tyypeille, joille se voi todistaa sen olevan turvallista. Erityistapaus, jälleen samankaltainen kuin Send:n ja Sync:n kanssa, on tilanne, jossa Unpin:iä ei toteuteta tyypille. Tämän merkintä on impl !Unpin for SomeType, jossa SomeType on tyypin nimi, joka tarvitsee ylläpitää näitä takeita ollakseen turvallinen aina, kun osoitin kyseiseen tyyppiin käytetään Pin:issä.

Toisin sanoen Pin:n ja Unpin:in suhteesta on pidettävä mielessä kaksi asiaa. Ensinnäkin Unpin on ”normaali” tapaus ja !Unpin on erikoistapaus. Toiseksi se, toteuttaako tyyppi Unpin:in vai !Unpin:in, merkitsee vain, kun käytät kiinnitettyä osoitinta kyseiseen tyyppiin, kuten Pin<&mut SomeType>.

Tehdään tästä konkreettista: ajattele String:iä. Siinä on pituus ja sen muodostavat Unicode-merkit. Voimme kääriä String:in Pin:iin, kuten kuvassa 17-8. String toteuttaa kuitenkin automaattisesti Unpin:in, kuten useimmat muut tyypit Rustissa.

A box labeled “Pin” on the left with an arrow going from it to a box labeled “String” on the right. The “String” box contains the data 5usize, representing the length of the string, and the letters “h”, “e”, “l”, “l”, and “o” representing the characters of the string “hello” stored in this String instance. A dotted rectangle surrounds the “String” box and its label, but not the “Pin” box.
Kuva 17-8: `String`:in kiinnittäminen; katkoviiva osoittaa, että `String` toteuttaa `Unpin`-traitin eikä siten ole kiinnitetty

Näin voimme tehdä asioita, jotka olisivat laittomia, jos String toteuttaisi !Unpin:in, kuten korvata yhden merkkijonon toisella täsmälleen samassa muistisijainnissa kuten kuvassa 17-9. Tämä ei riko Pin-sopimusta, koska String:illä ei ole sisäisiä viittauksia, jotka tekisivät siirtämisestä turvatonta. Siksi se toteuttaa Unpin:in eikä !Unpin:iä.

The same “hello” string data from the previous example, now labeled “s1” and grayed out. The “Pin” box from the previous example now points to a different String instance, one that is labeled “s2”, is valid, has a length of 7usize, and contains the characters of the string “goodbye”. s2 is surrounded by a dotted rectangle because it, too, implements the Unpin trait.
Kuva 17-9: `String`:in korvaaminen täysin eri `String`:illä muistissa

Nyt tiedämme tarpeeksi ymmärtääksemme listauksen 17-23 join_all-kutsusta raportoidut virheet. Alun perin yritimme siirtää async-lohkojen tuottamat futuret Vec<Box<dyn Future<Output = ()>>>:iin, mutta kuten olemme nähneet, näillä futureilla voi olla sisäisiä viittauksia, joten ne eivät automaattisesti toteuta Unpin:iä. Kun kiinnitämme ne, voimme välittää tuloksena olevan Pin-tyypin Vec:iin luottavaisin mielin, ettei futurejen taustalla olevaa dataa siirretä. Listaus 17-24 näyttää, miten koodi korjataan kutsumalla pin!-makroa siellä, missä kukin kolmesta futuresta määritellään, ja säätämällä trait-objektin tyyppiä.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-24/src/main.rs:here}}
}
Listing 17-24: Futurejen kiinnittäminen, jotta ne voidaan siirtää vektoriin

Tämä esimerkki kääntyy ja suorittuu nyt, ja voisimme lisätä tai poistaa futureja vektorista ajonaikana ja yhdistää ne kaikki.

Pin ja Unpin ovat tärkeimpiä matalan tason kirjastojen rakentamisessa tai kun rakennat itse ajoympäristön, eivät niinkään päivittäisessä Rust-koodissa. Kun näet nämä traitit virheilmoituksissa, sinulla on nyt kuitenkin parempi käsitys siitä, miten koodisi korjataan!

Huom: Tämä Pin:n ja Unpin:in yhdistelmä mahdollistaa koko luokan monimutkaisten tyyppien turvallisen toteuttamisen Rustissa, jotka muuten olisivat haastavia, koska ne ovat itseensä viittaavia. Pin:ia vaativat tyypit esiintyvät yleisimmin async-Rustissa tänään, mutta silloin tällöin saatat nähdä niitä myös muissa konteksteissa.

Pin:n ja Unpin:in toiminnan yksityiskohdat ja säännöt, joita niiden täytyy noudattaa, on käsitelty laajasti std::pin:in API-dokumentaatiossa, joten jos haluat oppia lisää, se on hyvä lähtökohta.

Jos haluat ymmärtää, miten asiat toimivat vielä tarkemmin taustalla, katso Asynchronous Programming in Rust -kirjan luvut 2 ja 4.

Stream-trait

Nyt kun ymmärrät Future-, Pin- ja Unpin-traitit syvemmin, voimme kääntää huomion Stream-traitiin. Kuten opit aiemmin luvussa, streamit muistuttavat asynkronisia iteraattoreita. Toisin kuin Iterator ja Future, Stream:illa ei kuitenkaan ole määrittelyä standardikirjastossa tämän kirjoitushetkellä, mutta futures-crate:stä on olemassa hyvin yleinen määrittely, jota käytetään koko ekosysteemissä.

Käydään läpi Iterator- ja Future-traitien määrittelyt ennen kuin katsomme, miten Stream-trait voisi yhdistää ne. Iterator:stä saamme käsitteen sarjasta: sen next-metodi tarjoaa Option<Self::Item>:in. Future:sta saamme käsitteen valmiudesta ajan kuluessa: sen poll-metodi tarjoaa Poll<Self::Output>:in. Esittääksemme kohteiden sarjaa, joka tulee valmiiksi ajan kuluessa, määrittelemme Stream-traitin, joka yhdistää nämä ominaisuudet:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Stream-trait määrittelee assosioituneen tyypin Item streamin tuottamien kohteiden tyypille. Tämä on samankaltainen kuin Iterator:issä, jossa kohteita voi olla nollasta moneen, ja erilainen kuin Future:issa, jossa on aina yksi Output, vaikka se olisi yksikkötyyppi ().

Stream määrittelee myös metodin näiden kohteiden hakemiseen. Kutsumme sitä poll_next:iksi korostaaksemme, että se pollaa samalla tavalla kuin Future::poll ja tuottaa kohteiden sarjan samalla tavalla kuin Iterator::next. Sen palautustyyppi yhdistää Poll:in ja Option:in. Ulompi tyyppi on Poll, koska valmius täytyy tarkistaa aivan kuten futuressa. Sisempi tyyppi on Option, koska sen täytyy ilmaista, onko viestejä enempää, aivan kuten iteraattorissa.

Jokin hyvin samankaltainen määrittely tulee todennäköisesti osaksi Rustin standardikirjastoa. Sillä välin se on osa useimpien ajoympäristöjen työkalupakkia, joten voit luottaa siihen, ja kaiken seuraavan pitäisi yleensä päteä!

”Streamit: futuret peräkkäin” -osion esimerkeissä emme kuitenkaan käyttäneet poll_next:iä tai Stream:iä, vaan next:iä ja StreamExt:iä. Voisimme työskennellä suoraan poll_next-API:n kanssa kirjoittamalla omat Stream-tilakoneemme käsin, aivan kuten voisimme työskennellä futurejen kanssa suoraan niiden poll-metodin kautta. await:in käyttö on kuitenkin paljon miellyttävämpää, ja StreamExt-trait tarjoaa next-metodin, jotta voimme tehdä juuri sen:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/no-listing-stream-ext/src/lib.rs:here}}
}

Huom: Todellinen määrittely, jota käytimme aiemmin luvussa, näyttää hieman erilaiselta, koska se tukee Rust-versioita, jotka eivät vielä tukeneet async-funktioiden käyttöä traiteissa. Sen vuoksi se näyttää tältä:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

Next-tyyppi on struct, joka toteuttaa Future:n ja antaa meille nimetä self-viittauksen eliniän Next<'_, Self>:llä, jotta await voi toimia tämän metodin kanssa.

StreamExt-trait on myös kaikkien streamien kanssa käytettävien mielenkiintoisten metodien koti. StreamExt toteutetaan automaattisesti jokaiselle tyypille, joka toteuttaa Stream:in, mutta nämä traitit on määritelty erikseen, jotta yhteisö voi iteroida kätevyys-API:ja vaikuttamatta perustavanlaatuiseen traitiin.

trpl-crate:ssä käytetyssä StreamExt-versiossa trait määrittelee next-metodin lisäksi myös oletustoteutuksen next:lle, joka käsittelee oikein Stream::poll_next:in kutsumisen yksityiskohdat. Tämä tarkoittaa, että vaikka joudut kirjoittamaan oman streamaavan tietotyypin, sinun täytyy toteuttaa vain Stream, ja kuka tahansa tietotyyppiäsi käyttävä voi käyttää StreamExt:iä ja sen metodeja automaattisesti.

Tämä on kaikki, mitä käsittelemme näiden traitien matalamman tason yksityiskohdista. Lopuksi tarkastellaan, miten futuret (mukaan lukien streamit), tehtävät ja säikeet sopivat yhteen!

Futures, tehtävät ja säikeet

Futures, Tasks ja Threads yhdessä

Kuten näimme luvussa 16, säikeet tarjoavat yhden lähestymistavan samanaikaisuuteen. Olemme nähneet tässä luvussa toisen lähestymistavan: asyncin käytön futurejen ja streamien kanssa. Jos mietit, milloin valita toinen menetelmä toisen sijaan, vastaus on: se riippuu tilanteesta! Ja monissa tapauksissa valinta ei ole säikeet tai async, vaan säikeet ja async.

Monet käyttöjärjestelmät ovat tarjonneet säikeisiin perustuvia samanaikaisuusmalleja jo vuosikymmeniä, ja monet ohjelmointikielet tukevat niitä sen vuoksi. Nämä mallit eivät kuitenkaan ole ilman kompromissejaan. Monissa käyttöjärjestelmissä jokainen säie käyttää melko paljon muistia. Säikeet ovat myös vaihtoehto vain, kun käyttöjärjestelmäsi ja laitteistosi tukevat niitä. Toisin kuin tavallisissa pöytä- ja mobiilitietokoneissa, joissakin sulautetuissa järjestelmissä ei ole lainkaan käyttöjärjestelmää, joten niissä ei ole myöskään säikeitä.

Async-malli tarjoaa erilaisen — ja lopulta toisiaan täydentävän — joukon kompromisseja. Async-mallissa samanaikaiset operaatiot eivät vaadi omia säikeitään. Sen sijaan ne voivat suorittua tehtävinä, kuten kun käytimme trpl::spawn_task:ia käynnistämään työtä synkronisesta funktiosta streamit-osiossa. Tehtävä on samankaltainen kuin säie, mutta sen sijaan että käyttöjärjestelmä hallinnoisi sitä, kirjastotason koodi hallinnoi sitä: ajoympäristö.

On syy siihen, miksi säikeiden ja tehtävien luomisen API:t ovat niin samankaltaisia. Säikeet toimivat rajoina synkronisten operaatioiden joukoille; samanaikaisuus on mahdollista säikeiden välillä. Tehtävät toimivat rajoina asynkronisten operaatioiden joukoille; samanaikaisuus on mahdollista sekä tehtävien välillä että sisällä, koska tehtävä voi vaihtaa futurejen välillä rungossaan. Lopuksi futuret ovat Rustin hienojakoisin samanaikaisuuden yksikkö, ja jokainen future voi edustaa muiden futurejen puuta. Ajoympäristö — erityisesti sen executor — hallinnoi tehtäviä, ja tehtävät hallinnoivat futureja. Tässä suhteessa tehtävät muistuttavat kevyitä, ajoympäristön hallinnoimia säikeitä, joilla on lisäominaisuuksia, koska ajoympäristö eikä käyttöjärjestelmä hallinnoi niitä.

Tämä ei tarkoita, että async-tehtävät olisivat aina parempia kuin säikeet (tai päinvastoin). Samanaikaisuus säikeillä on joissakin suhteissa yksinkertaisempi ohjelmointimalli kuin samanaikaisuus async:illa. Se voi olla vahvuus tai heikkous. Säikeillä ei ole luontaista vastinetta futurelle; ne suorittuvat yksinkertaisesti loppuun ilman keskeytyksiä paitsi käyttöjärjestelmän itsensä aiheuttamia.

Ja käy ilmi, että säikeet ja tehtävät toimivat usein hyvin yhdessä, koska tehtävät voidaan (ainakin joissakin ajoympäristöissä) siirtää säikeiden välillä. Itse asiassa taustalla käyttämämme ajoympäristö — mukaan lukien spawn_blocking- ja spawn_task-funktiot — on oletuksena monisäikeinen! Monet ajoympäristöt käyttävät lähestymistapaa nimeltä work stealing siirtääkseen tehtäviä läpinäkyvästi säikeiden välillä sen perusteella, miten säikeitä käytetään parhaillaan, parantaakseen järjestelmän kokonaissuorituskykyä. Tämä lähestymistapa vaatii itse asiassa sekä säikeitä että tehtäviä, ja siten myös futureja.

Kun mietit, mitä menetelmää käyttää milloin, harkitse näitä nyrkkisääntöjä:

  • Jos työ on hyvin rinnakkaistettavissa (eli laskentarajoitteista), kuten joukon datan käsittely, jossa kukin osa voidaan käsitellä erikseen, säikeet ovat parempi valinta.
  • Jos työ on hyvin samanaikaista (eli I/O-rajoitteista), kuten viestien käsittely useista eri lähteistä, jotka voivat saapua eri välein tai eri taajuudella, async on parempi valinta.

Ja jos tarvitset sekä rinnakkaisuutta että samanaikaisuutta, sinun ei tarvitse valita säikeiden ja asyncin välillä. Voit käyttää niitä vapaasti yhdessä ja antaa kummankin tehdä sen osan, jossa se on parhaimmillaan. Esimerkiksi listaus 17-25 näyttää melko tyypillisen esimerkin tämänkaltaisesta yhdistelmästä oikean maailman Rust-koodissa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch17-async-await/listing-17-25/src/main.rs:all}}
}
Listing 17-25: Viestien lähettäminen estävällä koodilla säikeessä ja viestien odottaminen async-lohkossa

Aloitamme luomalla async-kanavan ja luomalla säikeen, joka ottaa omistukseen kanavan lähettäjäpuolen move-avainsanalla. Säikeen sisällä lähetämme numerot 1–10 nukkuen sekunnin välein. Lopuksi suoritamme async-lohkoon välitetyllä futurella trpl::block_on:illa kuten koko luvun ajan. Tässä futuressa odotamme näitä viestejä aivan kuten muissa viestinvälitysesimerkeissä.

Palataksesi luvun alussa käsiteltyyn skenaarioon, kuvittele suorittavasi joukon videon koodaustehäviä omistetulla säikeellä (koska videon koodaus on laskentarajoitteista), mutta ilmoittavasi käyttöliittymälle async-kanavalla, kun nämä operaatiot ovat valmiita. Tällaisia yhdistelmiä on lukemattomia oikean maailman käyttötapauksissa.

Yhteenveto

Tämä ei ole viimeinen kerta, kun näet samanaikaisuutta tässä kirjassa. Luvun 21 projekti soveltaa näitä käsitteitä realistisemmassa tilanteessa kuin tässä käsitellyt yksinkertaisemmat esimerkit ja vertaa ongelmanratkaisua säikeillä sekä tehtävillä ja futureilla suoremmin.

Riippumatta siitä, minkä näistä lähestymistavoista valitset, Rust antaa työkalut turvallisen ja nopean samanaikaisen koodin kirjoittamiseen — olipa kyseessä suuren läpimenon web-palvelin tai sulautettu käyttöjärjestelmä.

Seuraavaksi puhumme idiomaattisista tavoista mallintaa ongelmia ja jäsentää ratkaisuja, kun Rust-ohjelmasi kasvavat. Lisäksi käsittelemme, miten Rustin idiomit liittyvät niihin, jotka saatat tuntea olio-ohjelmoinnista.

Rustin oliopohjaisen ohjelmoinnin piirteet

Oliopohjainen ohjelmointi (OOP) on tapa mallintaa ohjelmia. Olioiden käsite esiteltiin ohjelmointikielessä Simula 1960-luvulla. Nämä oliot vaikuttivat Alan Kayn kehittämään ohjelmointiarkkitehtuuriin, jossa oliot välittävät viestejä toisilleen. Kuvatakseen tätä arkkitehtuuria hän loi termin oliopohjainen ohjelmointi vuonna 1967.

OOP:sta on olemassa useita kilpailevia määritelmiä, ja joidenkin määritelmien mukaan Rust on oliopohjainen, mutta toisten mukaan ei. Tässä luvussa tarkastelemme tiettyjä omi…

Seuraavaksi tutkimme, kuinka toteuttaa oliopohjainen suunnittelumalli Rustilla ja keskustelemme siitä, milloin kannattaa käyttää Rustin omia vahvuuksia perinteisen OOP:n sijaan.

Oliopohjaisten kielten ominaisuudet

Oliopohjaisten kielten ominaisuudet

Ohjelmointiyhteisössä ei ole yksimielisyyttä siitä, mitä ominaisuuksia kielen täytyy sisältää ollakseen oliopohjainen. Rustiin on vaikuttanut monia ohjelmointiparadigmoja, mukaan lukien OOP; esimerkiksi tutkimme luvussa 13 funktionaalisen ohjelmoinnin tuomia ominaisuuksia. Voidaan väittää, että OOP-kielet jakavat tiettyjä yhteisiä piirteitä — nimittäin oliot, kapseloinnin ja periytymisen. Katsotaan, mitä kukin näistä ominaisuuksista tarkoittaa ja tukeeko Rust sitä.

Oliot sisältävät tietoa ja käyttäytymistä

Kirja Design Patterns: Elements of Reusable Object-Oriented Software Erich Gammalta, Richard Helmiltä, Ralph Johnsonilta ja John Vlissidesilta (Addison-Wesley, 1994), jota kutsutaan puhekielessä The Gang of Four -kirjaksi, on oliopohjaisten suunnittelumallien luettelo. Se määrittelee OOP:n näin:

Oliopohjaiset ohjelmat koostuvat olioista. Olio yhdistää sekä tiedon että sitä käsittelevät proseduurit. Näitä proseduurien kutsutaan tyypillisesti metodeiksi tai operaatioiksi.

Tämän määritelmän perusteella Rust on oliopohjainen: rakenteet ja luettelotyypit sisältävät tietoa, ja impl-lohkot tarjoavat metodeja rakenteille ja luettelotyypeille. Vaikka rakenteita ja luettelotyyppejä metodeineen ei kutsutakaan olioiksi, ne tarjoavat saman toiminnallisuuden Gang of Fourin olion määritelmän mukaan.

Kapselointi piilottaa toteutuksen yksityiskohdat

Toinen OOP:hen yleisesti liitettävä piirre on kapseloinnin idea, joka tarkoittaa, että olion toteutuksen yksityiskohdat eivät ole käytettävissä sitä käyttävälle koodille. Ainoa tapa olla vuorovaikutuksessa olion kanssa on sen julkinen rajapinta; olion käyttäjän koodin ei pitäisi pystyä kurkistamaan olion sisään ja muuttamaan tietoa tai käyttäytymistä suoraan. Näin ohjelmoija voi muuttaa ja refaktoroida olion sisäisyyksiä ilman, että olion käyttäjän koodia tarvitsee muuttaa.

Käsittelimme kapseloinnin hallintaa luvussa 7: voimme käyttää pub-avainsanaa päättääksemme, mitkä moduulit, tyypit, funktiot ja metodit koodissamme ovat julkisia, ja oletuksena kaikki muu on yksityistä. Voimme esimerkiksi määritellä rakenteen AveragedCollection, jolla on kenttä i32-arvojen vektorille. Rakenteella voi olla myös kenttä, joka sisältää vektorin arvojen keskiarvon, eli keskiarvoa ei tarvitse laskea aina tarvittaessa. Toisin sanoen AveragedCollection välimuistittaa lasketun keskiarvon puolestamme. Listauksessa 18-1 on AveragedCollection-rakenteen määritelmä.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-01/src/lib.rs}}
Listing 18-1: AveragedCollection-rakenne, joka ylläpitää kokonaislukulistaa ja kokoelman kohteiden keskiarvoa

Rakenne on merkitty pub-avainsanalla, jotta muu koodi voi käyttää sitä, mutta rakenteen kentät pysyvät yksityisinä. Tämä on tässä tapauksessa tärkeää, koska haluamme varmistaa, että aina kun listaan lisätään tai siitä poistetaan arvo, myös keskiarvo päivittyy. Teemme tämän toteuttamalla rakenteelle metodit add, remove ja average, kuten listauksessa 18-2 näytetään.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-02/src/lib.rs:here}}
Listing 18-2: Julkisten metodien add, remove ja average toteutukset AveragedCollection-rakenteelle

Julkiset metodit add, remove ja average ovat ainoat tavat käyttää tai muokata tietoa AveragedCollection-instanssissa. Kun add-metodilla lisätään kohde list-kenttään tai remove-metodilla poistetaan kohde, kummankin toteutus kutsuu yksityistä update_average-metodia, joka päivittää myös average-kentän.

Jätämme list- ja average-kentät yksityisiksi, jotta ulkopuolinen koodi ei voi lisätä tai poistaa kohteita suoraan list-kentästä; muuten average-kenttä voisi mennä epäsynkkaan, kun list muuttuu. average-metodi palauttaa average-kentän arvon, jolloin ulkopuolinen koodi voi lukea keskiarvon mutta ei muuttaa sitä.

Koska olemme kapseloineet AveragedCollection-rakenteen toteutuksen yksityiskohdat, voimme helposti muuttaa esimerkiksi tietorakennetta tulevaisuudessa. Voisimme esimerkiksi käyttää list-kentässä HashSet<i32>-tyyppiä Vec<i32>-tyypin sijaan. Niin kauan kuin julkisten metodien add, remove ja average allekirjoitukset pysyvät samoina, AveragedCollection-tyyppiä käyttävän koodin ei tarvitse muuttua. Jos tekisimme list-kentästä julkisen, näin ei välttämättä olisi: HashSet<i32>- ja Vec<i32>-tyypeillä on eri metodit kohteiden lisäämiseen ja poistamiseen, joten ulkopuolinen koodi todennäköisesti joutuisi muuttumaan, jos se muokkaisi list-kenttää suoraan.

Jos kapselointi on pakollinen ominaisuus, jotta kieli katsottaisiin oliopohjaiseksi, Rust täyttää tämän vaatimuksen. Mahdollisuus käyttää pub-avainsanaa tai olla käyttämättä sitä eri koodin osissa mahdollistaa toteutuksen yksityiskohtien kapseloinnin.

Periytyminen tyyppijärjestelmänä ja koodin jakamisena

Periytyminen on mekanismi, jossa olio voi periä elementtejä toisen olion määritelmästä ja siten saada yläluokan tiedot ja käyttäytymisen ilman, että niitä tarvitsee määritellä uudelleen.

Jos kielen täytyy sisältää periytyminen ollakseen oliopohjainen, Rust ei ole sellainen kieli. Rakenteen, joka perii yläluokan kentät ja metoditoteutukset, määrittelyyn ei ole tapaa ilman makron käyttöä.

Jos olet tottunut käyttämään periytymistä ohjelmointityökalupakissasi, voit kuitenkin käyttää Rustissa muita ratkaisuja riippuen siitä, miksi alun perin tavoittelit periytymistä.

Valitsisit periytymisen kahdesta pääsyystä. Toinen on koodin uudelleenkäyttö: voit toteuttaa tietyn käyttäytymisen yhdelle tyypille, ja periytyminen mahdollistaa saman toteutuksen käytön eri tyypille. Voit tehdä tämän rajatusti Rust-koodissa käyttämällä trait-metodien oletustoteutuksia, jotka näimme listauksessa 10-14, kun lisäsimme oletustoteutuksen summarize-metodille Summary-traitissa. Jokaisella Summary-traitin toteuttavalla tyypillä olisi summarize-metodi käytettävissä ilman lisäkoodia. Tämä muistuttaa tilannetta, jossa yläluokalla on metodin toteutus ja sitä perivällä alaluokalla on sama metodin toteutus. Voimme myös ylikirjoittaa summarize-metodin oletustoteutuksen, kun toteutamme Summary-traitin, mikä muistuttaa alaluokan ylikirjoittavan yläluokalta perityn metodin toteutuksen.

Toinen syy periytymisen käyttöön liittyy tyyppijärjestelmään: mahdollistaa alatyypin käytön samoissa paikoissa kuin ylätyyppiä. Tätä kutsutaan myös polymorfismiksi, mikä tarkoittaa, että voit korvata useita olioita toisillaan ajonaikana, jos niillä on tiettyjä yhteisiä ominaisuuksia.

Polymorfismi

Monille ihmisille polymorfismi on synonyymi periytymiselle. Se on kuitenkin yleisempi käsite, joka viittaa koodiin, joka voi toimia useiden eri tyyppien datan kanssa. Periytymisessä nämä tyypit ovat yleensä alaluokkia.

Rust käyttää sen sijaan geneerisyyttä abstrahoimaan eri mahdollisia tyyppejä ja trait-sidontoja asettaakseen rajoituksia sille, mitä näiden tyyppien täytyy tarjota. Tätä kutsutaan joskus rajoitetuksi parametriseksi polymorfismiksi.

Rust on valinnut erilaiset kompromissit tarjoamatta periytymistä. Periytyminen on usein vaarassa jakaa enemmän koodia kuin on tarpeen. Alaluokkien ei pitäisi aina jakaa kaikkia yläluokan ominaisuuksia, mutta periytymisessä ne tekevät niin. Tämä voi tehdä ohjelman suunnittelusta vähemmän joustavaa. Se tuo myös mahdollisuuden kutsua alaluokkien metodeja, jotka eivät ole järkeviä tai aiheuttavat virheitä, koska metodit eivät sovellu alaluokkaan. Lisäksi jotkin kielet sallivat vain yksinkertaisen periytymisen (eli alaluokka voi periä vain yhdestä luokasta), mikä rajoittaa entisestään ohjelman suunnittelun joustavuutta.

Näistä syistä Rust ottaa erilaisen lähestymistavan ja käyttää trait-olioita periytymisen sijaan polymorfismin saavuttamiseksi ajonaikana. Katsotaan, miten trait-oliot toimivat.

Trait-objektien käyttö, jotka sallivat eri tyyppisten arvojen käytön

Trait-olioiden käyttö yhteisen käyttäytymisen abstrahoimiseen

Luvussa 8 mainitsimme, että vektorien yksi rajoitus on, että ne voivat tallentaa vain yhden tyyppisiä elementtejä. Loimme kiertotien listauksessa 8-9 määrittelemällä SpreadsheetCell-luettelotyypin, jolla on variantteja kokonaislukujen, liukulukujen ja tekstin tallentamiseen. Näin voimme tallentaa eri tyyppistä dataa jokaiseen soluun ja silti pitää vektorin, joka edustaa soluriviä. Tämä on täysin hyvä ratkaisu, kun vaihdettavat kohteet ovat kiinteä joukko tyyppejä, jotka tiedämme käännösaikana.

Joskus haluamme kuitenkin, että kirjastomme käyttäjä voi laajentaa tiettyyn tilanteeseen kelpaavien tyyppien joukkoa. Näyttääksemme, miten tämän voisimme saavuttaa, luomme esimerkin graafisen käyttöliittymän (GUI) työkalusta, joka käy läpi kohteiden listan ja kutsuu jokaisella draw-metodia piirtääkseen sen näytölle — yleinen tekniikka GUI-työkaluissa. Luomme kirjastokrateen nimeltä gui, joka sisältää GUI-kirjaston rakenteen. Tämä krate voi sisältää joitakin tyyppejä ihmisten käyttöön, kuten Button tai TextField. Lisäksi gui-käyttäjät haluavat luoda omia piirrettäviä tyyppejä: esimerkiksi yksi ohjelmoija saattaa lisätä Image-tyypin ja toinen SelectBox-tyypin.

Kirjastoa kirjoittaessamme emme voi tietää ja määritellä kaikkia tyyppejä, joita muut ohjelmoijat saattavat haluta luoda. Tiedämme kuitenkin, että gui:n täytyy seurata monia eri tyyppisiä arvoja ja kutsua draw-metodia jokaisella näistä eri tyyppisistä arvoista. Sen ei tarvitse tietää tarkalleen, mitä tapahtuu kun kutsumme draw-metodia, vain että arvolla on kyseinen metodi käytettävissä.

Periytymistä käyttävässä kielessä voisimme määritellä luokan nimeltä Component, jolla on metodi draw. Muut luokat, kuten Button, Image ja SelectBox, perisivät Component-luokasta ja siten perisivät draw-metodin. Ne voisivat kukin ylikirjoittaa draw-metodin määritelläkseen oman käyttäytymisensä, mutta kehys voisi käsitellä kaikkia tyyppejä kuin ne olisivat Component-instansseja ja kutsua niillä draw-metodia. Koska Rustissa ei ole periytymistä, tarvitsemme toisen tavan rakentaa gui-kirjasto niin, että käyttäjät voivat luoda kirjaston kanssa yhteensopivia uusia tyyppejä.

Yhteisen käyttäytymisen traitin määrittely

Toteuttaaksemme haluamamme gui-käyttäytymisen määrittelemme traitin nimeltä Draw, jolla on yksi metodi nimeltä draw. Sitten voimme määritellä vektorin, joka ottaa trait-olion. Trait-olio osoittaa sekä määrittelemämme traitin toteuttavan tyypin instanssiin että taulukkoon, jota käytetään trait-metodien hakemiseen kyseiseltä tyypiltä ajonaikana. Luomme trait-olion määrittelemällä jonkinlaisen osoittimen, kuten viitteen tai Box<T>-älyosoittimen, sitten dyn-avainsanan ja sitten asiaankuuluvan traitin. (Puhumme syystä, miksi trait-olioiden täytyy käyttää osoitinta, kohdassa ”Dynaamisesti mitoitetut tyypit ja Sized-trait” luvussa 20.) Voimme käyttää trait-olioita geneerisen tai konkreettisen tyypin sijaan. Missä tahansa käytämme trait-oliota, Rustin tyyppijärjestelmä varmistaa käännösaikana, että kyseisessä kontekstissa käytetty arvo toteuttaa trait-olion traitin. Näin ollen emme tarvitse tietää kaikkia mahdollisia tyyppejä käännösaikana.

Olemme maininneet, että Rustissa vältämme kutsumasta rakenteita ja luettelotyyppejä ”olioiksi” erottaaksemme ne muiden kielten olioista. Rakenteessa tai luettelotyypissä rakenteen kentissä oleva data ja impl-lohkojen käyttäytyminen ovat erillään, kun taas muissa kielissä data ja käyttäytyminen yhdistettynä yhteen käsitteeseen merkitään usein olioksi. Trait-oliot eroavat muiden kielten olioista siinä, että trait-olioon ei voi lisätä dataa. Trait-oliot eivät ole yhtä yleisesti hyödyllisiä kuin muiden kielten oliot: niiden erityinen tarkoitus on mahdollistaa abstraktio yhteisen käyttäytymisen yli.

Listaus 18-3 näyttää, miten määritellään trait Draw yhdellä draw-metodilla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-03/src/lib.rs}}
Listing 18-3: Draw-traitin määritelmä

Tämän syntaksin pitäisi näyttää tutulta keskusteluistamme traitien määrittelystä luvussa 10. Seuraavaksi tulee uutta syntaksia: listaus 18-4 määrittelee rakenteen Screen, joka sisältää vektorin nimeltä components. Tämä vektori on tyyppiä Box<dyn Draw>, joka on trait-olio; se on sijaisarvo mille tahansa Box-sisällä olevalle tyypille, joka toteuttaa Draw-traitin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-04/src/lib.rs:here}}
Listing 18-4: Screen-rakenteen määritelmä, jossa components-kenttä sisältää Draw-traitin toteuttavien trait-olioiden vektorin

Screen-rakenteelle määrittelemme metodin run, joka kutsuu draw-metodia jokaisella components-kentän komponentilla, kuten listauksessa 18-5 näytetään.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-05/src/lib.rs:here}}
Listing 18-5: run-metodi Screen-rakenteella, joka kutsuu draw-metodia jokaisella komponentilla

Tämä toimii eri tavalla kuin rakenteen määrittely geneerisellä tyyppiparametrilla trait-sidonnoin. Geneerinen tyyppiparametri voidaan korvata vain yhdellä konkreettisella tyypillä kerrallaan, kun taas trait-oliot sallivat useiden konkreettisten tyyppien täyttää trait-olion paikan ajonaikana. Voisimme esimerkiksi määritellä Screen-rakenteen käyttämällä geneeristä tyyppiä ja trait-sidontaa, kuten listauksessa 18-6.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-06/src/lib.rs:here}}
Listing 18-6: Vaihtoehtoinen Screen-rakenteen ja sen run-metodin toteutus geneerisyyttä ja trait-sidontoja käyttäen

Tämä rajoittaa meidät Screen-instanssiin, jonka komponenttilista sisältää kaikki Button- tai kaikki TextField-tyyppisiä komponentteja. Jos sinulla on vain homogeenisia kokoelmia, geneerisyyden ja trait-sidontojen käyttö on parempi, koska määritelmät monomorfisoidaan käännösaikana käyttämään konkreettisia tyyppejä.

Toisaalta trait-olioita käyttävällä menetelmällä yksi Screen-instanssi voi sisältää Vec<T>-vektorin, jossa on sekä Box<Button> että Box<TextField>. Katsotaan, miten tämä toimii, ja puhumme sitten ajonaikaisen suorituskyvyn vaikutuksista.

Traitin toteuttaminen

Lisäämme nyt tyyppejä, jotka toteuttavat Draw-traitin. Tarjoamme Button-tyypin. Varsinaisen GUI-kirjaston toteuttaminen on tämän kirjan laajuuden ulkopuolella, joten draw-metodilla ei ole hyödyllistä toteutusta rungossaan. Kuvitellaksemme, miltä toteutus voisi näyttää, Button-rakenteella voi olla kentät width, height ja label, kuten listauksessa 18-7.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-07/src/lib.rs:here}}
Listing 18-7: Button-rakenne, joka toteuttaa Draw-traitin

Button-rakenteen kentät width, height ja label eroavat muiden komponenttien kentistä; esimerkiksi TextField-tyypillä voi olla samat kentät plus placeholder-kenttä. Jokainen tyyppi, jonka haluamme piirtää näytölle, toteuttaa Draw-traitin, mutta käyttää eri koodia draw-metodissa määritelläkseen, miten kyseinen tyyppi piirretään, kuten Button tässä (ilman varsinaista GUI-koodia, kuten mainittiin). Button-tyypillä voi esimerkiksi olla lisä-impl-lohko metodeille, jotka liittyvät siihen, mitä tapahtuu kun käyttäjä napsauttaa painiketta. Tällaiset metodit eivät sovellu tyypeille kuten TextField.

Jos joku kirjastomme käyttäjä päättää toteuttaa SelectBox-rakenteen, jolla on kentät width, height ja options, he toteuttaisivat Draw-traitin myös SelectBox-tyypille, kuten listauksessa 18-8.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-08/src/main.rs:here}}
Listing 18-8: Toinen krate käyttää gui-kirjastoa ja toteuttaa Draw-traitin SelectBox-rakenteelle

Kirjastomme käyttäjä voi nyt kirjoittaa main-funktionsa luodakseen Screen-instanssin. Screen-instanssiin he voivat lisätä SelectBox- ja Button-komponentit laittamalla kummankin Box<T>-säiliöön trait-olioksi. Sitten he voivat kutsua run-metodia Screen-instanssilla, joka kutsuu draw-metodia jokaisella komponentilla. Listaus 18-9 näyttää tämän toteutuksen.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-09/src/main.rs:here}}
Listing 18-9: Trait-olioiden käyttö eri tyyppisten, saman traitin toteuttavien arvojen tallentamiseen

Kirjastoa kirjoittaessamme emme tienneet, että joku saattaa lisätä SelectBox-tyypin, mutta Screen-toteutuksemme pystyi toimimaan uudella tyypillä ja piirtämään sen, koska SelectBox toteuttaa Draw-traitin, mikä tarkoittaa, että se toteuttaa draw-metodin.

Tämä käsite — huolehtia vain viesteistä, joihin arvo vastaa, eikä arvon konkreettisesta tyypistä — muistuttaa ankkutyyppauksen käsitettä dynaamisesti tyypitetyissä kielissä: jos se kävelee kuin ankka ja nokkuu kuin ankka, sen täytyy olla ankka! Screen-rakenteen run-toteutuksessa listauksessa 18-5 run ei tarvitse tietää kunkin komponentin konkreettista tyyppiä. Se ei tarkista, onko komponentti Button- tai SelectBox-instanssi, vaan kutsuu vain komponentin draw-metodia. Määrittelemällä components-vektorin arvojen tyypiksi Box<dyn Draw> olemme määritelleet Screen-rakenteen tarvitsevan arvoja, joilla voimme kutsua draw-metodia.

Trait-olioiden ja Rustin tyyppijärjestelmän käytön etu koodin kirjoittamisessa, joka muistuttaa ankkutyyppauksen käyttöä, on, ettemme koskaan joudu tarkistamaan ajonaikana, toteuttaako arvo tietyn metodin, emmekä huoli virheistä, jos arvo ei toteuta metodia mutta kutsumme sitä silti. Rust ei käännä koodiamme, jos arvot eivät toteuta trait-olioiden tarvitsemia traitteja.

Esimerkiksi listaus 18-10 näyttää, mitä tapahtuu, jos yritämme luoda Screen-rakenteen, jonka komponentti on String.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-10/src/main.rs}}
Listing 18-10: Yritys käyttää tyyppiä, joka ei toteuta trait-olion traitia

Saamme tämän virheen, koska String ei toteuta Draw-traitia:

{{#include ../listings/ch18-oop/listing-18-10/output.txt}}

Tämä virhe kertoo meille, että joko välitämme Screen-rakenteelle jotain, mitä emme tarkoittaneet, ja siksi pitäisi välittää eri tyyppi, tai meidän pitäisi toteuttaa Draw String-tyypille, jotta Screen voi kutsua sillä draw-metodia.

Dynaamisen lähetyksen suorittaminen

Muistutaan kohdasta ”Geneeristä koodia käyttävän koodin suorituskyky” luvussa 10 keskusteluamme kääntäjän geneerisyyksille suorittamasta monomorfisointiprosessista: kääntäjä generoi ei-geneerisiä toteutuksia funktioille ja metodeille jokaiselle konkreettiselle tyypille, jota käytämme geneerisen tyyppiparametrin sijaan. Monomorfisoinnista syntyvä koodi tekee staattista lähetystä, jossa kääntäjä tietää käännösaikana, mitä metodia kutsut. Tämä on vastakohta dynaamiselle lähetykselle, jossa kääntäjä ei voi käännösaikana kertoa, mitä metodia kutsut. Dynaamisen lähetyksen tapauksissa kääntäjä tuottaa koodia, joka ajonaikana tietää, mitä metodia kutsua.

Kun käytämme trait-olioita, Rustin täytyy käyttää dynaamista lähetystä. Kääntäjä ei tiedä kaikkia tyyppejä, joita voidaan käyttää trait-olioita käyttävän koodin kanssa, joten se ei tiedä, mitä tyypille toteutettua metodia kutsua. Sen sijaan ajonaikana Rust käyttää trait-olion sisällä olevia osoittimia tietääkseen, mitä metodia kutsua. Tämä haku aiheuttaa ajonaikaisen kustannuksen, jota ei esiinny staattisessa lähetyksessä. Dynaaminen lähetys estää myös kääntäjää valitsemasta metodin koodin sisällyttämistä, mikä puolestaan estää joitakin optimointeja, ja Rustilla on sääntöjä siitä, missä dynaamista lähetystä voi ja ei voi käyttää — näitä kutsutaan dyn-yhteensopivuudeksi. Nämä säännöt ovat tämän keskustelun laajuuden ulkopuolella, mutta voit lukea niistä lisää viitteestä. Saimme kuitenkin lisäjoustavuutta listauksessa 18-5 kirjoittamaamme koodiin ja pystyimme tukemaan listauksessa 18-9 esitettyä, joten se on harkittava kompromissi.

Oliopohjaisen suunnittelumallin toteuttaminen

Oliopohjaisen suunnittelumallin toteuttaminen

tilamalli on oliopohjainen suunnittelumalli. Mallin ydin on, että määrittelemme joukon tiloja, joissa arvo voi sisäisesti olla. Tilat edustetaan joukolla tilaolioita, ja arvon käyttäytyminen muuttuu sen tilan mukaan. Käymme läpi esimerkin blogikirjoitus-rakenteesta, jolla on kenttä tilan säilyttämiseen; tila on tilaolio joukosta ”luonnos”, ”tarkistus” tai ”julkaistu”.

Tilaoliot jakavat toiminnallisuutta: Rustissa käytämme tietysti rakenteita ja traitteja objektien ja periytymisen sijaan. Jokainen tilaolio vastaa omasta käyttäytymisestään ja siitä, milloin sen pitäisi siirtyä toiseen tilaan. Arvo, joka sisältää tilaolion, ei tiedä tilojen eri käyttäytymisestä tai siirtymisajankohdista.

Tilamallin etu on, että kun ohjelman liiketoimintavaatimukset muuttuvat, emme joudu muuttamaan tilaa sisältävän arvon koodia tai arvoa käyttävän koodin koodia. Meidän tarvitsee päivittää vain yhden tilaolion sisäinen koodi muuttaaksemme sen sääntöjä tai ehkä lisätäksemme uusia tilaolioita.

Ensin toteutamme tilamallin perinteisemmällä oliopohjaisella tavalla. Sitten käytämme lähestymistapaa, joka on luontevampi Rustissa. Aloitetaan toteuttamaan blogikirjoituksen työnkulkua vaiheittain tilamallia käyttäen.

Lopullinen toiminnallisuus näyttää tältä:

  1. Blogikirjoitus alkaa tyhjänä luonnoksena.
  2. Kun luonnos on valmis, kirjoituksen tarkistus pyydetään.
  3. Kun kirjoitus hyväksytään, se julkaistaan.
  4. Vain julkaistut blogikirjoitukset palauttavat tulostettavaa sisältöä, jotta hyväksymättömät kirjoitukset eivät voi vahingossa julkaista.

Mikä tahansa muu kirjoitukseen tehty muutosyritys ei saa vaikuttaa mihinkään. Jos esimerkiksi yritämme hyväksyä luonnoskirjoituksen ennen kuin olemme pyytäneet tarkistusta, kirjoituksen pitäisi pysyä julkaisemattomana luonnoksena.

Perinteisen oliopohjaisen tyylin yritys

Samaan ongelmaan on äärettömän monta tapaa rakentaa koodi, jokaisella eri kompromisseilla. Tämän osion toteutus on perinteisempää oliopohjaista tyyliä, joka on mahdollista kirjoittaa Rustissa, mutta ei hyödynnä kaikkia Rustin vahvuuksia. Myöhemmin esittelemme toisen ratkaisun, joka käyttää edelleen oliopohjaista suunnittelumallia mutta on rakennettu tavalla, joka saattaa näyttää vähemmän tutulta oliopohjaista kokemusta omaaville ohjelmoijille. Vertailemme kahta ratkaisua kokeaksemme kompromissit, kun Rust-koodia suunnitellaan eri tavalla kuin muiden kielten koodia.

Listaus 18-11 näyttää tämän työnkulun koodimuodossa: tämä on esimerkki blog-nimisen kirjastokrateen API:n käytöstä. Tämä ei vielä käänny, koska emme ole toteuttaneet blog-kratea.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:all}}
Listing 18-11: Koodi, joka demonstroi haluttua käyttäytymistä, jonka haluamme blog-krateen

Haluamme antaa käyttäjän luoda uuden luonnosblogikirjoituksen Post::new-funktiolla. Haluamme sallia tekstin lisäämisen blogikirjoitukseen. Jos yritämme hakea kirjoituksen sisältöä heti, ennen hyväksyntää, emme saa tekstiä, koska kirjoitus on vielä luonnos. Olemme lisänneet koodiin assert_eq!-makron demonstrointitarkoituksessa. Erinomainen yksikkötesti tälle olisi varmistaa, että luonnosblogikirjoitus palauttaa tyhjän merkkijonon content-metodista, mutta emme kirjoita testejä tähän esimerkkiin.

Seuraavaksi haluamme mahdollistaa kirjoituksen tarkistuksen pyytämisen ja haluamme, että content palauttaa tyhjän merkkijonon odottaessaan tarkistusta. Kun kirjoitus saa hyväksynnän, se julkaistaan, eli kirjoituksen teksti palautetaan kun content kutsutaan.

Huomaa, että ainoa tyyppi, jonka kanssa olemme vuorovaikutuksessa kratesta, on Post-tyyppi. Tämä tyyppi käyttää tilamallia ja sisältää arvon, joka on yksi kolmesta tilaoliosta edustaen eri tiloja, joissa kirjoitus voi olla — luonnos, tarkistus tai julkaistu. Siirtyminen tilasta toiseen hallitaan sisäisesti Post-tyypin sisällä. Tilat muuttuvat vastauksena kirjastomme käyttäjien Post-instanssilla kutsumiin metodeihin, mutta heidän ei tarvitse hallita tilamuutoksia suoraan. Lisäksi käyttäjät eivät voi tehdä virheitä tilojen kanssa, kuten julkaista kirjoitusta ennen tarkistusta.

Post-rakenteen määrittely ja uuden instanssin luominen

Aloitetaan kirjaston toteutus! Tiedämme tarvitsevamme julkisen Post-rakenteen, joka sisältää sisältöä, joten aloitamme rakenteen määritelmästä ja siihen liittyvästä julkisesta new-funktiosta Post-instanssin luomiseen, kuten listauksessa 18-12 näytetään. Teemme myös yksityisen State-traitin, joka määrittelee käyttäytymisen, joka kaikilla Post-rakenteen tilaolioilla täytyy olla.

Sitten Post sisältää trait-olion Box<dyn State> Option<T>-tyypin sisällä yksityisessä state-kentässä tilaolion säilyttämiseen. Näet hetken kuluttua, miksi Option<T> on tarpeen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-12/src/lib.rs}}
Listing 18-12: Post-rakenteen määritelmä ja new-funktio uuden Post-instanssin luomiseen, State-trait ja Draft-rakenne

State-trait määrittelee eri kirjoitustilojen jakaman käyttäytymisen. Tilaoliot ovat Draft, PendingReview ja Published, ja ne kaikki toteuttavat State-traitin. Toistaiseksi traitilla ei ole metodeja, ja aloitamme määrittelemällä vain Draft-tilan, koska siinä tilassa kirjoituksen pitää alkaa.

Kun luomme uuden Post-instanssin, asetamme sen state-kentän Some-arvoksi, joka sisältää Box-osoittimen. Tämä Box osoittaa uuteen Draft-rakenteen instanssiin. Tämä varmistaa, että aina kun luomme uuden Post-instanssin, se alkaa luonnoksena. Koska Post-rakenteen state-kenttä on yksityinen, Post-instanssia ei voi luoda missään muussa tilassa! Post::new-funktiossa asetamme content-kentän uudeksi tyhjäksi String-arvoksi.

Kirjoituksen sisällön tekstin tallentaminen

Näimme listauksessa 18-11, että haluamme pystyä kutsumaan add_text-nimistä metodia ja välittämään sille &str-viitteen, joka lisätään blogikirjoituksen tekstisisällöksi. Toteutamme tämän metodina sen sijaan, että paljastaisimme content-kentän pub-avainsanalla, jotta voimme myöhemmin toteuttaa metodin, joka hallitsee content-kentän datan lukemista. add_text-metodi on melko suoraviivainen, joten lisätään toteutus listauksessa 18-13 impl Post -lohkoon.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-13/src/lib.rs:here}}
Listing 18-13: add_text-metodin toteutus tekstin lisäämiseksi kirjoituksen content-kenttään

add_text-metodi ottaa muuttuvan viitteen self:ään, koska muutamme Post-instanssia, jolla kutsumme add_text-metodia. Kutsumme sitten push_str-metodia content-kentän String-arvolla ja välitämme text-argumentin lisättäväksi tallennettuun content-arvoon. Tämä käyttäytyminen ei riipu kirjoituksen tilasta, joten se ei ole osa tilamallia. add_text-metodi ei ole lainkaan vuorovaikutuksessa state-kentän kanssa, mutta se on osa käyttäytymistä, jota haluamme tukea.

Luonnoskirjoituksen sisällön tyhjyyden varmistaminen

Vaikka olisimme kutsuneet add_text-metodia ja lisänneet sisältöä kirjoitukseen, haluamme silti content-metodin palauttavan tyhjän merkkijonoviitteen, koska kirjoitus on vielä luonnostilassa, kuten listauksen 18-11 ensimmäinen assert_eq! osoittaa. Toteutetaan toistaiseksi content-metodi yksinkertaisimmalla tavalla, joka täyttää tämän vaatimuksen: palauttamalla aina tyhjän merkkijonoviitteen. Muutamme tämän myöhemmin, kun toteutamme kyvykkyyden muuttaa kirjoituksen tilaa julkaistavaksi. Toistaiseksi kirjoitukset voivat olla vain luonnostilassa, joten kirjoituksen sisällön pitäisi aina olla tyhjä. Listaus 18-14 näyttää tämän väliaikaisen toteutuksen.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-14/src/lib.rs:here}}
Listing 18-14: Väliaikaisen toteutuksen lisääminen content-metodille Post-rakenteella, joka palauttaa aina tyhjän merkkijonoviitteen

Tämän lisätyn content-metodin myötä kaikki listauksessa 18-11 ensimmäiseen assert_eq!-kutsuun asti toimii tarkoitetulla tavalla.

Tarkistuksen pyytäminen, joka muuttaa kirjoituksen tilaa

Seuraavaksi meidän täytyy lisätä toiminnallisuus kirjoituksen tarkistuksen pyytämiseen, mikä pitäisi muuttaa sen tilan Draft-tilasta PendingReview-tilaan. Listaus 18-15 näyttää tämän koodin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-15/src/lib.rs:here}}
Listing 18-15: request_review-metodien toteutus Post-rakenteella ja State-traitilla

Annamme Post-rakenteelle julkisen request_review-nimisen metodin, joka ottaa muuttuvan viitteen self:ään. Kutsumme sitten sisäistä request_review-metodia Post-rakenteen nykyisellä tilalla, ja tämä toinen request_review-metodi kuluttaa nykyisen tilan ja palauttaa uuden tilan.

Lisäämme request_review-metodin State-traitiin; kaikkien traitin toteuttavien tyyppien täytyy nyt toteuttaa request_review-metodi. Huomaa, että metodin ensimmäisenä parametrina on self: Box<Self> sen sijaan, että olisi self, &self tai &mut self. Tämä syntaksi tarkoittaa, että metodi on kelvollinen vain kun sitä kutsutaan Box-osoittimella, joka sisältää tyypin. Tämä syntaksi ottaa omistajuuden Box<Self>-arvosta, mitätöiden vanhan tilan, jotta Post-rakenteen tilan arvo voi muuttua uudeksi tilaksi.

Vanhan tilan kuluttamiseksi request_review-metodin täytyy ottaa omistajuus tilan arvosta. Tässä Post-rakenteen state-kentän Option tulee kuvaan: kutsumme take-metodia ottamaan Some-arvon state-kentästä ja jättämään sen paikalle None-arvon, koska Rust ei salli tyhjäksi jääneitä kenttiä rakenteissa. Näin voimme siirtää state-arvon pois Post-rakenteesta lainaamisen sijaan. Asetamme sitten kirjoituksen state-arvon tämän operaation tulokseksi.

Meidän täytyy asettaa state väliaikaisesti None-arvoksi sen sijaan, että asettaisimme sen suoraan koodilla kuten self.state = self.state.request_review(); saadaksemme omistajuuden state-arvosta. Tämä varmistaa, että Post ei voi käyttää vanhaa state-arvoa sen jälkeen, kun olemme muuttaneet sen uudeksi tilaksi.

Draft-rakenteen request_review-metodi palauttaa uuden, laatikkoon pakatun instanssin uudesta PendingReview-rakenteesta, joka edustaa tilaa, jossa kirjoitus odottaa tarkistusta. PendingReview-rakenne toteuttaa myös request_review-metodin, mutta ei tee muunnoksia. Sen sijaan se palauttaa itsensä, koska kun pyydämme tarkistusta kirjoitukselle, joka on jo PendingReview-tilassa, sen pitäisi pysyä PendingReview-tilassa.

Nyt voimme alkaa nähdä tilamallin edut: Post-rakenteen request_review-metodi on sama riippumatta sen state-arvosta. Jokainen tila vastaa omista säännöistään.

Jätämme Post-rakenteen content-metodin ennalleen palauttaen tyhjän merkkijonoviitteen. Voimme nyt olla Post-instanssi PendingReview-tilassa sekä Draft-tilassa, mutta haluamme saman käyttäytymisen PendingReview-tilassa. Listaus 18-11 toimii nyt toiseen assert_eq!-kutsuun asti!

approve-metodin lisääminen content-metodin käyttäytymisen muuttamiseksi

approve-metodi on samanlainen kuin request_review-metodi: se asettaa state-kentän arvoksi sen, jonka nykyinen tila sanoo sen pitävän olla kun tila hyväksytään, kuten listauksessa 18-16 näytetään.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-16/src/lib.rs:here}}
Listing 18-16: approve-metodin toteutus Post-rakenteella ja State-traitilla

Lisäämme approve-metodin State-traitiin ja uuden rakenteen, joka toteuttaa State-traitin, Published-tilan.

Samalla tavalla kuin PendingReview-rakenteen request_review toimii, jos kutsumme approve-metodia Draft-rakenteella, sillä ei ole vaikutusta, koska approve palauttaa self:n. Kun kutsumme approve-metodia PendingReview-rakenteella, se palauttaa uuden, laatikkoon pakatun instanssin Published-rakenteesta. Published-rakenne toteuttaa State-traitin, ja sekä request_review- että approve-metodille se palauttaa itsensä, koska kirjoituksen pitäisi pysyä Published-tilassa näissä tapauksissa.

Nyt meidän täytyy päivittää Post-rakenteen content-metodi. Haluamme content-metodin palauttaman arvon riippuvan Post-rakenteen nykyisestä tilasta, joten annamme Post-rakenteen delegoida content-metodille, joka on määritelty sen state-kentässä, kuten listauksessa 18-17 näytetään.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-17/src/lib.rs:here}}
Listing 18-17: Post-rakenteen content-metodin päivittäminen delegoimaan State-traitin content-metodille

Koska tavoitteena on pitää kaikki nämä säännöt State-traitin toteuttavien rakenteiden sisällä, kutsumme content-metodia state-kentän arvolla ja välitämme kirjoitusinstanssin (eli self) argumenttina. Palautamme sitten arvon, jonka state-arvon content-metodin käyttö palauttaa.

Kutsumme as_ref-metodia Option-tyypillä, koska haluamme viitteen Option-tyypin sisällä olevaan arvoon omistajuuden sijaan. Koska state on Option<Box<dyn State>>, kun kutsumme as_ref-metodia, palautuu Option<&Box<dyn State>>. Jos emme kutsuisi as_ref-metodia, saisimme virheen, koska emme voi siirtää state-arvoa pois funktion parametrin lainatusta &self-viitteestä.

Kutsumme sitten unwrap-metodia, joka emme tiedä koskaan panikoivan, koska tiedämme Post-rakenteen metodien varmistavan, että state sisältää aina Some-arvon kun nämä metodit ovat valmiit. Tämä on yksi tapauksista, joista puhuimme luvun 9 ”Kun sinulla on enemmän tietoa kuin kääntäjällä” -osiossa, kun tiedämme None-arvon olevan mahdoton, vaikka kääntäjä ei pysty sitä ymmärtämään.

Tässä vaiheessa, kun kutsumme content-metodia &Box<dyn State>-arvolla, dereferointipakotus vaikuttaa &- ja Box-osoittimiin, joten content-metodia kutsutaan lopulta tyypillä, joka toteuttaa State-traitin. Tämä tarkoittaa, että meidän täytyy lisätä content State-traitin määritelmään, ja sinne asetamme logiikan sille, mitä sisältöä palautetaan riippuen siitä, mikä tila meillä on, kuten listauksessa 18-18 näytetään.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-18/src/lib.rs:here}}
Listing 18-18: content-metodin lisääminen State-traitiin

Lisäämme content-metodille oletustoteutuksen, joka palauttaa tyhjän merkkijonoviitteen. Tämä tarkoittaa, että emme tarvitse toteuttaa content-metodia Draft- ja PendingReview-rakenteille. Published-rakenne ylikirjoittaa content-metodin ja palauttaa arvon post.content-kentästä. Vaikka kätevää, State-traitin content-metodin käyttäminen Post-rakenteen sisällön määrittämiseen hämärtää rajaa State-traitin ja Post-rakenteen vastuiden välillä.

Huomaa, että tarvitsemme elinaikaannotaatiot tälle metodille, kuten käsittelimme luvussa 10. Otamme viitteen post-argumenttiin ja palautamme viitteen osaan kyseisestä post-argumentista, joten palautetun viitteen elinaika liittyy post-argumentin elinaikaan.

Ja olemme valmiit — koko listaus 18-11 toimii nyt! Olemme toteuttaneet tilamallin blogikirjoituksen työnkulun säännöillä. Sääntöihin liittyvä logiikka asuu tilaolioissa sen sijaan, että se olisi hajautettu ympäri Post-rakennetta.

Miksi ei luettelotyyppiä?

Olet ehkä miettinyt, miksi emme käyttäneet luettelotyyppiä eri mahdollisilla kirjoitustiloilla variantteina. Se on varmasti mahdollinen ratkaisu; kokeile sitä ja vertaa lopputuloksia nähdäksesi kumman pidät parempana! Yksi luettelotyypin käytön haittapuoli on, että jokaisessa paikassa, joka tarkistaa luettelotyypin arvon, tarvitaan match-lauseke tai vastaava käsittelemään jokainen mahdollinen variantti. Tämä voi tulla toistuvammaksi kuin tämä trait-olio-ratkaisu.

Tilamallin arviointi

Olemme osoittaneet, että Rust pystyy toteuttamaan oliopohjaisen tilamallin kapseloimaan erilaiset käyttäytymiset, joita kirjoituksella pitäisi olla kussakin tilassa. Post-rakenteen metodit eivät tiedä eri käyttäytymisistä. Koodin organisoinnin ansiosta meidän tarvitsee katsoa vain yhteen paikkaan tietääksemme eri tavat, joilla julkaistu kirjoitus voi käyttäytyä: Published-rakenteen State-traitin toteutus.

Jos loisimme vaihtoehtoisen toteutuksen, joka ei käyttäisi tilamallia, voisimme sen sijaan käyttää match-lausekkeita Post-rakenteen metodeissa tai jopa main-koodissa, joka tarkistaa kirjoituksen tilan ja muuttaa käyttäytymistä näissä paikoissa. Tämä tarkoittaisi, että meidän pitäisi katsoa useita paikkoja ymmärtääksemme kaikki seuraukset siitä, että kirjoitus on julkaistussa tilassa.

Tilamallin kanssa Post-rakenteen metodit ja paikat, joissa käytämme Post-rakennetta, eivät tarvitse match-lausekkeita, ja uuden tilan lisäämiseksi meidän tarvitsee vain lisätä uusi rakenne ja toteuttaa trait-metodit kyseiselle rakenteelle yhdessä paikassa.

Tilamallia käyttävä toteutus on helppo laajentaa lisätoiminnallisuudella. Nähdäksesi tilamallia käyttävän koodin ylläpidon yksinkertaisuuden, kokeile muutamia näistä ehdotuksista:

  • Lisää reject-metodi, joka muuttaa kirjoituksen tilan PendingReview-tilasta takaisin Draft-tilaan.
  • Vaadi kaksi approve-kutsua ennen kuin tila voi muuttua Published-tilaksi.
  • Salli käyttäjien lisätä tekstisisältöä vain kun kirjoitus on Draft-tilassa. Vihje: anna tilaolion vastata siitä, mikä sisällössä saattaa muuttua, mutta älä anna sen vastata Post-rakenteen muokkaamisesta.

Yksi tilamallin haittapuoli on, että koska tilat toteuttavat siirtymät tilojen välillä, jotkin tilat ovat kytkettyjä toisiinsa. Jos lisäisimme uuden tilan PendingReview- ja Published-tilojen väliin, kuten Scheduled, meidän pitäisi muuttaa PendingReview-rakenteen koodia siirtymään Scheduled-tilaan sen sijaan. Olisi vähemmän työtä, jos PendingReview-rakenteen ei tarvitsisi muuttua uuden tilan lisäämisen yhteydessä, mutta se tarkoittaisi siirtymistä toiseen suunnittelumalliin.

Toinen haittapuoli on, että olemme monistaneet logiikkaa. Monistuksen poistamiseksi voisimme yrittää tehdä oletustoteutukset request_review- ja approve-metodeille State-traitissa, jotka palauttavat self:n. Tämä ei kuitenkaan toimisi: kun käytämme State-traitia trait-oliona, trait ei tiedä tarkalleen, mikä konkreettinen self on, joten palautustyyppi ei ole tiedossa käännösaikana. (Tämä on yksi dyn-yhteensopivuussäännöistä, joista mainittiin aiemmin.)

Muuta monistusta ovat samanlaiset request_review- ja approve-metodien toteutukset Post-rakenteella. Molemmat metodit käyttävät Option::take-metodia Post-rakenteen state-kentällä, ja jos state on Some, ne delegoivat käärityn arvon saman metodin toteutukselle ja asettavat state-kentän uudeksi arvoksi tuloksen. Jos Post-rakenteella olisi paljon tämän kaavan mukaisia metodeja, voisimme harkita makron määrittelyä toiston poistamiseksi (katso luvun 20 ”Makrot” -osio).

Toteuttamalla tilamallin täsmälleen niin kuin se on määritelty oliopohjaisille kielille, emme hyödynnä Rustin vahvuuksia niin täysin kuin voisimme. Katsotaan muutoksia, joita voimme tehdä blog-krateen, jotta virheelliset tilat ja siirtymät muuttuvat käännösaikaisiksi virheiksi.

Tilojen ja käyttäytymisen koodaus tyyppeinä

Näytämme, miten tilamalli voidaan ajatella uudelleen saadaksemme erilaisen joukon kompromisseja. Sen sijaan, että kapseloisimme tilat ja siirtymät kokonaan niin, ettei ulkopuolinen koodi tiedä niistä mitään, koodaamme tilat eri tyyppeihin. Näin Rustin tyyppitarkistusjärjestelmä estää yritykset käyttää luonnoskirjoituksia paikoissa, joissa sallitaan vain julkaistut kirjoitukset, antamalla kääntäjävirheen.

Tarkastellaan listauksen 18-11 main-funktion ensimmäistä osaa:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:here}}

Sallimme edelleen uusien kirjoitusten luomisen luonnostilassa Post::new-funktiolla ja tekstin lisäämisen kirjoituksen sisältöön. Sen sijaan, että luonnoskirjoituksella olisi content-metodi, joka palauttaa tyhjän merkkijonon, teemme niin, ettei luonnoskirjoituksilla ole content-metodia lainkaan. Näin, jos yritämme hakea luonnoskirjoituksen sisältöä, saamme kääntäjävirheen, joka kertoo metodin olevan olematon. Näin on mahdotonta vahingossa näyttää luonnoskirjoituksen sisältöä tuotannossa, koska sellainen koodi ei edes käänny. Listaus 18-19 näyttää Post- ja DraftPost-rakenteiden määritelmät sekä metodit kummallekin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-19/src/lib.rs}}
Listing 18-19: Post-rakenne content-metodilla ja DraftPost-rakenne ilman content-metodia

Sekä Post- että DraftPost-rakenteilla on yksityinen content-kenttä, joka tallentaa blogikirjoituksen tekstin. Rakenteilla ei ole enää state-kenttää, koska siirrämme tilan koodauksen rakenteiden tyyppeihin. Post-rakenne edustaa julkaistua kirjoitusta, ja sillä on content-metodi, joka palauttaa content-kentän arvon.

Meillä on edelleen Post::new-funktio, mutta sen sijaan, että se palauttaisi Post-instanssin, se palauttaa DraftPost-instanssin. Koska content on yksityinen eikä ole funktioita, jotka palauttavat Post-tyypin, Post-instanssia ei voi luoda tällä hetkellä.

DraftPost-rakenteella on add_text-metodi, joten voimme lisätä tekstiä content-kenttään kuten ennenkin, mutta huomaa, että DraftPost-rakenteella ei ole määritelty content-metodia! Nyt ohjelma varmistaa, että kaikki kirjoitukset alkavat luonnoskirjoituksina, eikä luonnoskirjoituksilla ole sisältöään näytettävissä. Mikä tahansa yritys kiertää nämä rajoitukset johtaa kääntäjävirheeseen.

Miten siis saamme julkaistun kirjoituksen? Haluamme pakottaa säännön, että luonnoskirjoituksen täytyy tarkistaa ja hyväksyä ennen julkaisemista. Tarkistusta odottavan tilan kirjoituksen ei pitäisi silti näyttää sisältöä. Toteutamme nämä rajoitukset lisäämällä toisen rakenteen, PendingReviewPost, määrittelemällä request_review-metodin DraftPost-rakenteelle palauttamaan PendingReviewPost-rakenteen ja määrittelemällä approve-metodin PendingReviewPost-rakenteelle palauttamaan Post-rakenteen, kuten listauksessa 18-20 näytetään.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-20/src/lib.rs:here}}
Listing 18-20: PendingReviewPost, joka luodaan kutsumalla request_review-metodia DraftPost-rakenteella, ja approve-metodi, joka muuttaa PendingReviewPost-rakenteen julkaistuksi Post-rakenteeksi

request_review- ja approve-metodit ottavat omistajuuden self:stä, kuluttaen DraftPost- ja PendingReviewPost-instanssit ja muuttaen ne PendingReviewPost- ja julkaistuksi Post-rakenteeksi. Näin meillä ei jää jäljelle DraftPost-instansseja sen jälkeen, kun olemme kutsuneet niillä request_review-metodia, ja niin edelleen. PendingReviewPost-rakenteella ei ole määritelty content-metodia, joten sen sisällön lukeminen johtaa kääntäjävirheeseen, kuten DraftPost-rakenteella. Koska ainoa tapa saada julkaistu Post-instanssi, jolla on määritelty content-metodi, on kutsua approve-metodia PendingReviewPost-rakenteella, ja ainoa tapa saada PendingReviewPost on kutsua request_review-metodia DraftPost-rakenteella, olemme nyt koodanneet blogikirjoituksen työnkulun tyyppijärjestelmään.

Meidän täytyy kuitenkin tehdä pieniä muutoksia main-funktioon. request_review- ja approve-metodit palauttavat uusia instansseja sen sijaan, että muokkaisivat rakennetta, jolla niitä kutsutaan, joten meidän täytyy lisätä enemmän let post = -varjostusmäärityksiä tallentaaksemme palautetut instanssit. Emme myöskään voi tehdä väitteitä luonnos- ja tarkistusta odottavien kirjoitusten sisällöstä tyhjinä merkkijonoina, emmekä tarvitse niitä: emme voi enää kääntää koodia, joka yrittää käyttää kirjoitusten sisältöä näissä tiloissa. Päivitetty koodi main-funktiossa on listauksessa 18-21.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch18-oop/listing-18-21/src/main.rs}}
Listing 18-21: Muutokset main-funktioon käyttämään blogikirjoituksen työnkulun uutta toteutusta

Muutokset, jotka meidän piti tehdä main-funktioon post-muuttujan uudelleenmääritykseen, tarkoittavat, että tämä toteutus ei aivan seuraa oliopohjaista tilamallia enää: siirtymät tilojen välillä eivät ole enää täysin kapseloitu Post-rakenteen toteutuksen sisällä. Saamme kuitenkin sen, että virheelliset tilat ovat nyt mahdottomia tyyppijärjestelmän ja käännösaikaisen tyyppitarkistuksen ansiosta! Tämä varmistaa, että tietyt bugit, kuten julkaisemattoman kirjoituksen sisällön näyttäminen, löydetään ennen kuin ne päätyvät tuotantoon.

Kokeile tämän osion alussa ehdotettuja tehtäviä blog-krateen sellaisena kuin se on listauksen 18-21 jälkeen ja pohdi, mitä mieltä olet tämän version koodin suunnittelusta. Huomaa, että jotkin tehtävistä saattavat olla jo valmiita tässä suunnittelussa.

Olemme nähneet, että vaikka Rust pystyy toteuttamaan oliopohjaisia suunnittelumalleja, myös muut mallit, kuten tilan koodaus tyyppijärjestelmään, ovat saatavilla Rustissa. Näillä malleilla on erilaiset kompromissit. Vaikka olisit hyvin perehtynyt oliopohjaisiin malleihin, ongelman uudelleenajattelu Rustin ominaisuuksien hyödyntämiseksi voi tarjota etuja, kuten joidenkin bugien estämisen käännösaikana. Oliopohjaiset mallit eivät aina ole paras ratkaisu Rustissa tiettyjen ominaisuuksien, kuten omistajuuden, vuoksi, joita oliopohjaisilla kielillä ei ole.

Yhteenveto

Riippumatta siitä, pidätkö Rustia oliopohjaisena kielenä tämän luvun lukemisen jälkeen, tiedät nyt, että voit käyttää trait-olioita saadaksesi joitakin oliopohjaisia ominaisuuksia Rustissa. Dynaaminen dispatch voi antaa koodillesi joustavuutta pienen ajonaikaisen suorituskyvyn kustannuksella. Voit käyttää tätä joustavuutta oliopohjaisten mallien toteuttamiseen, jotka voivat auttaa koodisi ylläpidettävyydessä. Rustissa on myös muita ominaisuuksia, kuten omistajuus, joita oliopohjaisilla kielillä ei ole. Oliopohjainen malli ei aina ole paras tapa hyödyntää Rustin vahvuuksia, mutta se on käytettävissä oleva vaihtoehto.

Seuraavaksi katsomme kuvioita, jotka ovat toinen Rustin ominaisuuksista mahdollistamassa paljon joustavuutta. Olemme vilkaisseet niitä lyhyesti läpi kirjan, mutta emme ole vielä nähneet niiden täyttä potentiaalia. Mennään!

Käyttämällä malleja enemmän kontrollia virheenkäsittelyssä

Rustin mallinmukaisuuden (pattern matching) avulla voimme hallita ohjelman virheenkäsittelyä ja rakennetta tehokkaasti. Rust tarjoaa erilaisia tapoja käyttää malleja, kuten match, if let ja let-lausetta, joiden avulla voimme tehdä koodista selkeämpää ja turvallisempaa.

Tässä luvussa tutkimme seuraavia aiheita:

  • Kuinka Rustin mallit toimivat ja mitä etuja ne tarjoavat
  • Rustin eri tapoja soveltaa mallinmukaisuutta koodin kirjoittamisessa
  • Kehittyneempiä tapoja hallita ohjelman virhetilanteita ja tiedon suodatusta

Tarkastellaan ensin, miten Rustin mallinmukaisuus toimii käytännössä ja miksi se on tehokas tapa kirjoittaa koodia.

Kaikki paikat, joissa malleja voidaan käyttää

Kaikki paikat, joissa malleja voidaan käyttää

Mallit esiintyvät monissa paikoissa Rustissa, ja olet käyttänyt niitä paljon huomaamattasi! Tässä osiossa käsitellään kaikkia paikkoja, joissa mallit ovat sallittuja.

match-haarat

Kuten käsittelimme luvussa 6, käytämme malleja match-lausekkeiden haaroissa. Muodollisesti match-lausekkeet määritellään avainsanalla match, arvolla, jota vasten verrataan, ja yhdellä tai useammalla match-haaralla, jotka koostuvat mallista ja lausekkeesta, joka suoritetaan, jos arvo vastaa kyseisen haaran mallia, näin:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

Esimerkiksi tässä on match-lauseke listauksesta 6-5, joka vertaa muuttujan x Option<i32>-arvoa:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

Tämän match-lausekkeen mallit ovat None ja Some(i) kunkin nuolen vasemmalla puolella.

Yksi match-lausekkeiden vaatimuksista on, että niiden täytyy olla tyhjentäviä siinä mielessä, että kaikki match-lausekkeen arvon mahdollisuudet on käsiteltävä. Yksi tapa varmistaa, että olet kattanut jokaisen mahdollisuuden, on käyttää viimeisessä haarassa catch-all-mallia: Esimerkiksi mikä tahansa arvo vastaa muuttujan nimeä eikä voi koskaan epäonnistua, joten se kattaa kaikki jäljellä olevat tapaukset.

Erityinen malli _ vastaa mitä tahansa, mutta se ei koskaan sido muuttujaan, joten sitä käytetään usein viimeisessä match-haarassa. _-malli voi olla hyödyllinen, kun haluat jättää huomiotta minkä tahansa määrittelemättömän arvon, esimerkiksi. Käsittelemme _-mallin tarkemmin kohdassa ”Arvojen huomiotta jättäminen mallissa” myöhemmin tässä luvussa.

let-lauseet

Ennen tätä lukua olemme käsitelleet eksplisiittisesti vain mallien käyttöä match- ja if let -rakenteissa, mutta itse asiassa olemme käyttäneet malleja myös muissa paikoissa, mukaan lukien let-lauseissa. Harkitse esimerkiksi tätä suoraviivaista muuttujan sijoittamista let-lauseella:

#![allow(unused)]
fn main() {
let x = 5;
}

Joka kerta kun olet käyttänyt tällaista let-lausetta, olet käyttänyt malleja, vaikka et ehkä ole tajunnut sitä! Muodollisesti let-lause näyttää tältä:

let PATTERN = EXPRESSION;

Lausekkeissa kuten let x = 5;, joissa PATTERN-paikalla on muuttujan nimi, muuttujan nimi on vain erityisen yksinkertainen muoto mallista. Rust vertaa lauseketta malliin ja sijoittaa löytämänsä nimet. Esimerkissä let x = 5; x on malli, joka tarkoittaa ”sido tähän vastaava arvo muuttujaan x”. Koska nimi x on koko malli, tämä malli käytännössä tarkoittaa ”sido kaikki muuttujaan x, mikä arvo tahansa onkin.”

Nähdäksemme let-lauseen mallinmukaisuuspuolen selkeämmin, tarkastele listaus 19-1, joka käyttää mallia let-lauseessa purkaakseen tuplen.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-01/src/main.rs:here}}
}
Listing 19-1: Mallin käyttö tuplen purkamiseen ja kolmen muuttujan luomiseen kerralla

Tässä vertaamme tuplea malliin. Rust vertaa arvoa (1, 2, 3) malliin (x, y, z) ja näkee, että arvo vastaa mallia — eli elementtien määrä on sama molemmissa — joten Rust sitoo 1:n muuttujaan x, 2:n muuttujaan y ja 3:n muuttujaan z. Voit ajatella tämän tuple-mallin sisältävän kolme yksittäistä muuttujamallia sisäkkäin.

Jos mallin elementtien määrä ei vastaa tuplen elementtien määrää, kokonaistyyppi ei vastaa ja saamme kääntäjävirheen. Esimerkiksi listaus 19-2 näyttää yrityksen purkaa kolmielementtisen tuplen kahteen muuttujaan, mikä ei toimi.

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-02/src/main.rs:here}}
Listing 19-2: Virheellinen malli, jonka muuttujien määrä ei vastaa tuplen elementtien määrää

Tämän koodin kääntäminen tuottaa seuraavan tyyppivirheen:

{{#include ../listings/ch19-patterns-and-matching/listing-19-02/output.txt}}

Virheen korjaamiseksi voisimme jättää yhden tai useamman tuplen arvoista huomiotta käyttämällä _ tai .., kuten näet kohdassa ”Arvojen huomiotta jättäminen mallissa”. Jos ongelma on, että mallissa on liikaa muuttujia, ratkaisu on saada tyypit vastaamaan toisiaan poistamalla muuttujia niin, että muuttujien määrä vastaa tuplen elementtien määrää.

Ehdolliset if let -lausekkeet

Luvussa 6 käsittelimme, miten if let -lausekkeita käytetään pääasiassa lyhyempänä tapana kirjoittaa vastaava match, joka vastaa vain yhtä tapausta. Valinnaisesti if let -lausekkeella voi olla vastaava else, joka sisältää koodin, joka suoritetaan, jos if let -lausekkeen malli ei vastaa.

Listaus 19-3 osoittaa, että on myös mahdollista yhdistellä if let-, else if- ja else if let -lausekkeita. Näin saamme enemmän joustavuutta kuin match-lausekkeella, jossa voimme vertailla vain yhtä arvoa malleihin. Lisäksi Rust ei vaadi, että if let-, else if- ja else if let -haarojen sarjan ehdot liittyisivät toisiinsa.

Listauksen 19-3 koodi määrittää taustavärin useiden ehtojen tarkistusten perusteella. Tässä esimerkissä olemme luoneet muuttujat kovakoodatuilla arvoilla, jotka oikea ohjelma saattaisi saada käyttäjän syötteestä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-03/src/main.rs}}
}
Listing 19-3: if let-, else if-, else if let- ja else-rakenteiden yhdistäminen

Jos käyttäjä määrittää suosikkivärin, sitä käytetään taustavärinä. Jos suosikkiväriä ei ole määritelty ja tänään on tiistai, taustaväri on vihreä. Muussa tapauksessa, jos käyttäjä antaa ikänsä merkkijonona ja voimme jäsentää sen numeroksi onnistuneesti, väri on joko violetti tai oranssi riippuen luvun arvosta. Jos mikään näistä ehdoista ei täyty, taustaväri on sininen.

Tämä ehdollinen rakenne mahdollistaa monimutkaisten vaatimusten tukemisen. Tässä olevilla kovakoodatuilla arvoilla tämä esimerkki tulostaa Using purple as the background color.

Näet, että if let voi myös esitellä uusia muuttujia, jotka varjostavat olemassa olevia muuttujia samalla tavalla kuin match-haarat: rivi if let Ok(age) = age esittelee uuden age-muuttujan, joka sisältää Ok-variantin sisällä olevan arvon varjostaen olemassa olevan age-muuttujan. Tämä tarkoittaa, että ehdon if age > 30 täytyy sijoittaa kyseisen lohkon sisään: emme voi yhdistää näitä kahta ehtoa muotoon if let Ok(age) = age && age > 30. Uusi age, jota haluamme verrata lukuun 30, ei ole voimassa ennen kuin uusi alue alkaa aaltosulkeilla.

if let -lausekkeiden haittapuoli on, että kääntäjä ei tarkista tyhjentävyyttä, kun taas match-lausekkeissa se tekee niin. Jos jättäisimme pois viimeisen else-lohkon ja siten jättäisimme käsittelemättä joitakin tapauksia, kääntäjä ei varoittaisi meitä mahdollisesta logiikkavirheestä.

Ehdolliset while let -silmukat

Rakenteeltaan samankaltainen kuin if let, ehdollinen while let -silmukka sallii while-silmukan toimia niin kauan kuin malli vastaa edelleen. Listauksessa 19-4 näytämme while let -silmukan, joka odottaa säikeiden välillä lähetettyjä viestejä, mutta tässä tapauksessa tarkistaa Result-arvon Option-arvon sijaan.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-04/src/main.rs:here}}
}
Listing 19-4: while let -silmukan käyttö arvojen tulostamiseen niin kauan kuin rx.recv() palauttaa Ok-arvon

Tämä esimerkki tulostaa 1, 2 ja sitten 3. recv-metodi ottaa ensimmäisen viestin kanavan vastaanottopuolelta ja palauttaa Ok(value)-arvon. Kun näimme recv-metodin ensimmäisen kerran luvussa 16, purimme virheen suoraan tai käytimme sitä iteraattorina for-silmukassa. Kuten listaus 19-4 osoittaa, voimme kuitenkin käyttää myös while let -rakennetta, koska recv-metodi palauttaa Ok-arvon aina kun viesti saapuu niin kauan kuin lähettäjä on olemassa, ja tuottaa sitten Err-arvon, kun lähettäjäpuoli katkaisee yhteyden.

for-silmukat

for-silmukassa avainsanaa for seuraava arvo on malli. Esimerkiksi lausekkeessa for x in y arvo x on malli. Listaus 19-5 osoittaa, miten mallia käytetään for-silmukassa purkamaan eli erottelemaan tuplen osiin osana for-silmukkaa.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-05/src/main.rs:here}}
}
Listing 19-5: Mallin käyttö for-silmukassa tuplen purkamiseen

Listauksen 19-5 koodi tulostaa seuraavan:

{{#include ../listings/ch19-patterns-and-matching/listing-19-05/output.txt}}

Mukautamme iteraattoria enumerate-metodilla niin, että se tuottaa arvon ja kyseisen arvon indeksin tuplena. Ensimmäinen tuotettu arvo on tuple (0, 'a'). Kun tämä arvo vastaa mallia (index, value), index on 0 ja value on 'a', mikä tulostaa tulosteen ensimmäisen rivin.

Funktioiden parametrit

Funktioiden parametrit voivat myös olla malleja. Listauksen 19-6 koodi, joka määrittelee funktion nimeltä foo, jolla on yksi parametri nimeltä x tyypiltään i32, pitäisi nyt näyttää tutulta.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-06/src/main.rs:here}}
}
Listing 19-6: Funktioallekirjoitus, jossa parametreissa käytetään malleja

Osa x on malli! Kuten let-lauseessa, voimme vastata tuplea funktion argumenteissa malliin. Listaus 19-7 erottelee tuplen arvot, kun välitämme sen funktiolle.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-07/src/main.rs}}
}
Listing 19-7: Funktio, jonka parametrit purkavat tuplen

Tämä koodi tulostaa Current location: (3, 5). Arvot &(3, 5) vastaavat mallia &(x, y), joten x on arvo 3 ja y on arvo 5.

Voimme käyttää malleja myös sulkeumien parametri listoissa samalla tavalla kuin funktioiden parametri listoissa, koska sulkeumat ovat samankaltaisia kuin funktiot, kuten käsittelimme luvussa 13.

Tähän mennessä olet nähnyt useita tapoja käyttää malleja, mutta mallit eivät toimi samalla tavalla kaikissa paikoissa, joissa niitä voi käyttää. Joissakin paikoissa mallien täytyy olla kiistämättömiä; toisissa olosuhteissa ne voivat olla kumottavia. Käsittelemme nämä kaksi käsitettä seuraavaksi.

Kumoavuus: voiko malli epäonnistua vastaamaan

Kumoavuus: voiko malli epäonnistua vastaamaan

Malleja on kahdessa muodossa: kumottavia ja kiistämättömiä. Mallit, jotka vastaavat mitä tahansa mahdollista välitettyä arvoa, ovat kiistämättömiä. Esimerkki olisi x lausekkeessa let x = 5;, koska x vastaa mitä tahansa eikä siksi voi epäonnistua vastaamaan. Mallit, jotka voivat epäonnistua vastaamaan joitakin mahdollisia arvoja, ovat kumottavia. Esimerkki olisi Some(x) lausekkeessa if let Some(x) = a_value, koska jos muuttujan a_value arvo on None eikä Some, malli Some(x) ei vastaa.

Funktioiden parametrit, let-lauseet ja for-silmukat hyväksyvät vain kiistämättömiä malleja, koska ohjelma ei voi tehdä mitään järkevää, kun arvot eivät vastaa. if let- ja while let -lausekkeet sekä let...else- lause hyväksyvät kumottavia ja kiistämättömiä malleja, mutta kääntäjä varoittaa kiistämättömistä malleista, koska ne on määritelmän mukaan tarkoitettu käsittelemään mahdollista epäonnistumista: ehdollisen rakenteen toiminta perustuu kykyyn käyttäytyä eri tavalla onnistumisen ja epäonnistumisen mukaan.

Yleensä sinun ei tarvitse huolehtia erosta kumottavien ja kiistämättömien mallien välillä; sinun täytyy kuitenkin tuntea kumoavuuden käsite, jotta voit reagoida, kun näet sen virheilmoituksessa. Näissä tapauksissa sinun täytyy muuttaa joko mallia tai rakennetta, jossa käytät mallia, riippuen koodin tarkoitetusta käyttäytymisestä.

Katsotaan esimerkkiä siitä, mitä tapahtuu, kun yritämme käyttää kumottavaa mallia siellä, missä Rust vaatii kiistämätöntä mallia, ja päinvastoin. Listaus 19-8 näyttää let-lauseen, mutta malliksi olemme määrittäneet Some(x), kumottavan mallin. Kuten saattaa odottaa, tämä koodi ei käänny.

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-08/src/main.rs:here}}
Listing 19-8: Yritys käyttää kumottavaa mallia let-lauseessa

Jos some_option_value olisi None-arvo, se ei vastaisi mallia Some(x), mikä tarkoittaa, että malli on kumottava. let-lause voi kuitenkin hyväksyä vain kiistämättömän mallin, koska koodilla ei ole mitään järkevää tekemistä None-arvolla. Käännösaikana Rust valittaa, että olemme yrittäneet käyttää kumottavaa mallia siellä, missä kiistämätön malli vaaditaan:

{{#include ../listings/ch19-patterns-and-matching/listing-19-08/output.txt}}

Koska emme kattaneet (emmekä voineet kattaa!) jokaista kelvollista arvoa mallilla Some(x), Rust oikeutetusti tuottaa kääntäjävirheen.

Jos meillä on kumottava malli siellä, missä kiistämätön malli tarvitaan, voimme korjata sen muuttamalla mallia käyttävää koodia: let-lauseen sijaan voimme käyttää let...else-lausetta. Jos malli ei vastaa, aaltosulkeiden sisällä oleva koodi käsittelee arvon. Listaus 19-9 näyttää, miten listauksen 19-8 koodi korjataan.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-09/src/main.rs:here}}
}
Listing 19-9: let...else-lauseen ja lohkon käyttö kumottavien mallien kanssa let-lauseen sijaan

Olemme antaneet koodille ulospääsyn! Tämä koodi on täysin kelvollinen, vaikka se tarkoittaakin, ettemme voi käyttää kiistämätöntä mallia ilman varoitusta. Jos annamme let...else-lauseelle mallin, joka vastaa aina, kuten x, kuten listauksessa 19-10, kääntäjä antaa varoituksen.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-10/src/main.rs:here}}
}
Listing 19-10: Yritys käyttää kiistämätöntä mallia let...else-lauseessa

Rust valittaa, ettei let...else-lauseen käyttäminen kiistämättömän mallin kanssa ole järkevää:

{{#include ../listings/ch19-patterns-and-matching/listing-19-10/output.txt}}

Tästä syystä match-haarojen täytyy käyttää kumottavia malleja, paitsi viimeisessä haarassa, joka vastaa jäljellä olevia arvoja kiistämättömällä mallilla. Rust sallii kiistämättömän mallin käytön match-lausekkeessa, jossa on vain yksi haara, mutta tämä syntaksi ei ole erityisen hyödyllinen ja sen voisi korvata yksinkertaisemmalla let-lauseella.

Nyt kun tiedät, missä malleja käytetään ja mikä on ero kumottavien ja kiistämättömien mallien välillä, käydään läpi kaikki syntaksi, jota voimme käyttää mallien luomiseen.

Mallin syntaksi

Mallin syntaksi

Tässä osiossa kokoamme yhteen kaiken syntaksin, joka on sallittu malleissa, ja käsittelemme, miksi ja milloin saatat haluta käyttää kutakin.

Literaalien vastaaminen

Kuten näit luvussa 6, voit vastata malleja suoraan literaaleja vasten. Seuraava koodi antaa joitakin esimerkkejä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-01-literals/src/main.rs:here}}
}

Tämä koodi tulostaa one, koska x:n arvo on 1. Tämä syntaksi on hyödyllinen, kun haluat koodisi suorittavan toiminnon, jos se saa tietyn konkreettisen arvon.

Nimetyt muuttujat

Nimetyt muuttujat ovat kiistämättömiä malleja, jotka vastaavat mitä tahansa arvoa, ja olemme käyttäneet niitä monesti tässä kirjassa. On kuitenkin yksi monimutkaisuus, kun käytät nimettyjä muuttujia match-, if let- tai while let -lausekkeissa. Koska jokainen näistä lauseketyypeistä aloittaa uuden alueen, lausekkeen sisällä mallissa määritellyt muuttujat varjostavat samannimiset muuttujat rakenteen ulkopuolella, kuten kaikki muuttujat. Listauksessa 19-11 määrittelemme muuttujan nimeltä x arvolla Some(5) ja muuttujan y arvolla 10. Luomme sitten match-lausekkeen x:n arvolle. Katso match-haarojen malleja ja lopun println!-kutsua ja yritä arvata, mitä koodi tulostaa, ennen kuin suoritat koodin tai luet eteenpäin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-11/src/main.rs:here}}
}
Listing 19-11: match-lauseke, jonka haara esittelee uuden muuttujan, joka varjostaa olemassa olevan muuttujan y

Käydään läpi, mitä tapahtuu, kun match-lauseke suoritetaan. Ensimmäisen match-haaran malli ei vastaa x:n määriteltyä arvoa, joten koodi jatkuu.

Toisen match-haaran malli esittelee uuden muuttujan nimeltä y, joka vastaa mitä tahansa arvoa Some-arvon sisällä. Koska olemme uudella alueella match-lausekkeen sisällä, tämä on uusi y-muuttuja, ei alussa arvolla 10 määrittelemämme y. Tämä uusi y-sidonta vastaa mitä tahansa arvoa Some-arvon sisällä, mikä on se, mitä meillä on x:ssä. Siksi tämä uusi y sitoutuu x:n Some-arvon sisäiseen arvoon. Tuo arvo on 5, joten kyseisen haaran lauseke suoritetaan ja tulostaa Matched, y = 5.

Jos x olisi ollut None-arvo Some(5):n sijaan, ensimmäisten kahden haaran mallit eivät olisi vastanneet, joten arvo olisi vastannut alaviivaan. Emme esitelleet x-muuttujaa alaviiva-haaran mallissa, joten lausekkeen x on edelleen ulompi x, jota ei ole varjostettu. Tässä hypoteettisessa tapauksessa match tulostaisi Default case, x = None.

Kun match-lauseke on valmis, sen alue päättyy, ja samoin sisemmän y:n alue. Viimeinen println! tuottaa at the end: x = Some(5), y = 10.

Luodaksemme match-lausekkeen, joka vertaa ulompien x- ja y-muuttujien arvoja sen sijaan, että esittelisimme uuden muuttujan, joka varjostaa olemassa olevan y-muuttujan, meidän täytyisi käyttää match-ehtoa. Puhumme match-ehdoista myöhemmin kohdassa ”Lisäehtoja match-ehtojen avulla”.

Useiden mallien vastaaminen

match-lausekkeissa voit vastata useita malleja käyttämällä |-syntaksia, joka on mallien tai-operaattori. Esimerkiksi seuraavassa koodissa vastaamme x:n arvoa match-haaroihin, joista ensimmäisessä on tai-vaihtoehto, mikä tarkoittaa, että jos x:n arvo vastaa kumman tahansa arvon kyseisessä haarassa, kyseisen haaran koodi suoritetaan:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-02-multiple-patterns/src/main.rs:here}}
}

Tämä koodi tulostaa one or two.

Arvoalueiden vastaaminen ..=-syntaksilla

..=-syntaksi antaa meidän vastata sisältävään arvoalueeseen. Seuraavassa koodissa, kun malli vastaa mitä tahansa annetun alueen arvoista, kyseinen haara suoritetaan:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-03-ranges/src/main.rs:here}}
}

Jos x on 1, 2, 3, 4 tai 5, ensimmäinen haara vastaa. Tämä syntaksi on kätevämpi useille vastaamisarvoille kuin saman idean ilmaiseminen |-operaattorilla; jos käyttäisimme |, meidän pitäisi määritellä 1 | 2 | 3 | 4 | 5. Alueen määrittely on paljon lyhyempää, varsinkin jos haluamme vastata esimerkiksi minkä tahansa luvun väliltä 1–1000!

Kääntäjä tarkistaa käännösaikana, ettei alue ole tyhjä, ja koska ainoat tyypit, joille Rust voi kertoa, onko alue tyhjä vai ei, ovat char ja numeeriset arvot, alueet ovat sallittuja vain numeerisille tai char-arvoille.

Tässä on esimerkki char-arvoalueiden käytöstä:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-04-ranges-of-char/src/main.rs:here}}
}

Rust voi kertoa, että 'c' on ensimmäisen mallin alueella, ja tulostaa early ASCII letter.

Purkaminen arvojen erotteluun

Voimme myös käyttää malleja purkaaksemme rakenteita, enumeja ja tupleja käyttääksemme näiden arvojen eri osia. Käydään läpi jokainen arvotyyppi.

Rakenteet

Listaus 19-12 näyttää Point-rakenteen, jossa on kentät x ja y, jotka voimme purkaa erilleen mallilla let-lausekkeessa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-12/src/main.rs}}
}
Listing 19-12: Rakenteen kenttien purkaminen erillisiksi muuttujiksi

Tämä koodi luo muuttujat a ja b, jotka vastaavat p-rakenteen kenttien x ja y arvot. Tämä esimerkki osoittaa, etteivät mallin muuttujien nimet tarvitse vastata rakenteen kenttien nimiä. On kuitenkin yleistä vastata muuttujien nimet kenttien nimiin, jotta on helpompi muistaa, mistä kentistä muuttujat tulivat. Tämän yleisen käytännön vuoksi ja koska let Point { x: x, y: y } = p; sisältää paljon toistoa, Rustissa on oikotie rakenteen kenttiin vastaaville malleille: sinun tarvitsee vain listata rakenteen kentän nimi, ja mallista luodut muuttujat saavat samat nimet. Listaus 19-13 käyttäytyy samalla tavalla kuin listauksen 19-12 koodi, mutta let-mallissa luodut muuttujat ovat x ja y a:n ja b:n sijaan.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-13/src/main.rs}}
}
Listing 19-13: Rakenteen kenttien purkaminen rakenteen kenttien oikotiellä

Tämä koodi luo muuttujat x ja y, jotka vastaavat p-muuttujan kentät x ja y. Lopputuloksena muuttujat x ja y sisältävät arvot p-rakenteesta.

Voimme myös purkaa literaaliarvoja osana rakenteen mallia sen sijaan, että loisimme muuttujia kaikille kentille. Näin voimme testata joitakin kenttiä tiettyjä arvoja vasten samalla kun luomme muuttujia purkaaksemme muut kentät.

Listauksessa 19-14 meillä on match-lauseke, joka jakaa Point-arvot kolmeen tapaukseen: pisteet, jotka ovat suoraan x-akselilla (tosi kun y = 0), y-akselilla (x = 0) tai kummallakaan.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-14/src/main.rs:here}}
}
Listing 19-14: Purkaminen ja literaaliarvojen vastaaminen yhdessä mallissa

Ensimmäinen haara vastaa minkä tahansa pisteen, joka on x-akselilla määrittämällä, että y-kenttä vastaa, jos sen arvo vastaa literaaliarvoa 0. Malli luo silti x-muuttujan, jota voimme käyttää tämän haaran koodissa.

Vastaavasti toinen haara vastaa minkä tahansa pisteen y-akselilla määrittämällä, että x-kenttä vastaa, jos sen arvo on 0, ja luo y-muuttujan y-kentän arvolle. Kolmas haara ei määrittele literaaleja, joten se vastaa minkä tahansa muun Point-arvon ja luo muuttujat sekä x- että y-kentille.

Tässä esimerkissä arvo p vastaa toista haaraa, koska x sisältää arvon 0, joten tämä koodi tulostaa On the y axis at 7.

Muista, että match-lauseke lopettaa haarojen tarkistamisen, kun se on löytänyt ensimmäisen vastaavan mallin, joten vaikka Point { x: 0, y: 0 } olisi sekä x- että y-akselilla, tämä koodi tulostaisi vain On the x axis at 0.

Enumit

Olemme purkaneet enumeja tässä kirjassa (esimerkiksi listaus 6-5 luvussa 6), mutta emme ole vielä eksplisiittisesti käsitelleet, että enumin purkamiseen käytettävä malli vastaa enumiin tallennetun datan määrittelytapaa. Esimerkkinä listauksessa 19-15 käytämme Message-enumia listauksesta 6-2 ja kirjoitamme match-lausekkeen malleilla, jotka purkavat jokaisen sisäisen arvon.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-15/src/main.rs}}
}
Listing 19-15: Eri tyyppisiä arvoja sisältävien enum-varianttien purkaminen

Tämä koodi tulostaa Change color to red 0, green 160, and blue 255. Kokeile muuttaa msg:n arvoa nähdäksesi muiden haarojen koodin suorituksen.

Enum-varianteille, joilla ei ole dataa, kuten Message::Quit, emme voi purkaa arvoa enempää. Voimme vain vastata literaaliarvoa Message::Quit, eikä kyseisessä mallissa ole muuttujia.

Rakenteen kaltaisille enum-varianteille, kuten Message::Move, voimme käyttää rakenteisiin vastaamiseen samankaltaista mallia. Variantin nimen jälkeen laitamme aaltosulkeet ja listamme kentät muuttujilla, jotta voimme purkaa osat käytettäväksi tämän haaran koodissa. Tässä käytämme oikotietä kuten listauksessa 19-13.

Tuple-tyyppisille enum-varianteille, kuten Message::Write, joka sisältää tuplen yhdellä elementillä, ja Message::ChangeColor, joka sisältää tuplen kolmella elementillä, malli on samankaltainen kuin tupleihin vastaamiseen käytettävä malli. Mallin muuttujien määrän täytyy vastata variantin elementtien määrää, jota vastaamme.

Sisäkkäiset rakenteet ja enumit

Tähän asti esimerkkimme ovat vastanneet rakenteita tai enumeja yhdellä tasolla, mutta vastaaminen toimii myös sisäkkäisille alkioille! Voimme esimerkiksi refaktoroida listauksen 19-15 koodin tukemaan RGB- ja HSV-värejä ChangeColor-viestissä, kuten listauksessa 19-16.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-16/src/main.rs}}
}
Listing 19-16: Sisäkkäisiin enumeihin vastaaminen

match-lausekkeen ensimmäisen haaran malli vastaa Message::ChangeColor- enum-variantin, joka sisältää Color::Rgb-variantin; malli sitoutuu sitten kolmeen sisäiseen i32-arvoon. Toisen haaran malli vastaa myös Message::ChangeColor-enum-variantin, mutta sisäinen enum vastaa Color::Hsv:tä. Voimme määritellä nämä monimutkaiset ehdot yhdessä match-lausekkeessa, vaikka mukana on kaksi enumia.

Rakenteet ja tuplet

Voimme sekoittaa, vastata ja sisäkkäistää purkumalleja vielä monimutkaisemmilla tavoilla. Seuraava esimerkki näyttää monimutkaisen purkamisen, jossa sisäkkäistämme rakenteita ja tupleja tuplen sisään ja puramme kaikki primitiiviarvot:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-05-destructuring-structs-and-tuples/src/main.rs:here}}
}

Tämä koodi antaa meidän purkaa monimutkaiset tyypit osiinsa, jotta voimme käyttää kiinnostavia arvoja erikseen.

Purkaminen malleilla on kätevä tapa käyttää arvojen osia, kuten kunkin rakenteen kentän arvoa, erillään toisistaan.

Arvojen huomiotta jättäminen mallissa

Olet nähnyt, että on joskus hyödyllistä jättää arvoja huomiotta mallissa, kuten match-lausekkeen viimeisessä haarassa, saadaksesi catch-all-haaran, joka ei varsinaisesti tee mitään mutta kattaa kaikki jäljellä olevat mahdolliset arvot. On muutamia tapoja jättää kokonaisia arvoja tai arvojen osia huomiotta mallissa: käyttämällä _-mallia (jonka olet nähnyt), käyttämällä _-mallia toisen mallin sisällä, käyttämällä alaviivalla alkavaa nimeä tai käyttämällä ..-syntaksia jättääksemme huomiotta arvon jäljellä olevat osat. Tutkitaan, miten ja miksi käyttää kutakin näistä malleista.

Kokonainen arvo _:llä

Olemme käyttäneet alaviivaa jokerimallina, joka vastaa minkä tahansa arvon mutta ei sido arvoon. Tämä on erityisen hyödyllistä match-lausekkeen viimeisenä haarana, mutta voimme käyttää sitä missä tahansa mallissa, mukaan lukien funktioiden parametreissa, kuten listauksessa 19-17.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-17/src/main.rs}}
}
Listing 19-17: _:n käyttö funktioallekirjoituksessa

Tämä koodi jättää kokonaan huomiotta ensimmäisenä argumenttina välitetyn arvon 3 ja tulostaa This code only uses the y parameter: 4.

Useimmissa tapauksissa, kun et enää tarvitse tiettyä funktioparametria, muuttaisit allekirjoituksen niin, ettei se sisällä käyttämätöntä parametria. Funktioparametrin huomiotta jättäminen voi olla erityisen hyödyllistä tapauksissa, joissa esimerkiksi toteutat traitia, jolloin tarvitset tietyn tyyppiallekirjoituksen, mutta toteutuksesi funktion runko ei tarvitse yhtä parametreista. Näin vältät kääntäjän varoituksen käyttämättömistä funktioparametreista, jonka saisit, jos käyttäisit nimeä sen sijaan.

Arvon osat sisäkkäisellä _:llä

Voimme myös käyttää _:tä toisen mallin sisällä jättääksemme huomiotta vain osan arvosta, esimerkiksi kun haluamme testata vain osan arvosta mutta emme tarvitse muita osia vastaavassa koodissa, jonka haluamme suorittaa. Listaus 19-18 näyttää koodin, joka vastaa asetuksen arvon hallinnasta. Liiketoimintavaatimus on, että käyttäjä ei saa ylikirjoittaa olemassa olevaa asetuksen mukautusta, mutta voi poistaa asetuksen ja antaa sille arvon, jos se on tällä hetkellä asettamaton.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-18/src/main.rs:here}}
}
Listing 19-18: Alaviivan käyttö malleissa, jotka vastaavat Some-variantteja, kun emme tarvitse Some:n sisällä olevaa arvoa

Tämä koodi tulostaa Can't overwrite an existing customized value ja sitten setting is Some(5). Ensimmäisessä match-haarassa emme tarvitse vastata tai käyttää kummankaan Some-variantin sisällä olevia arvoja, mutta meidän täytyy testata tapaus, jossa sekä setting_value että new_setting_value ovat Some-variantteja. Siinä tapauksessa tulostamme syyn, miksi setting_value:a ei muuteta, eikä sitä muuteta.

Kaikissa muissa tapauksissa (jos jompikumpi setting_value:sta tai new_setting_value:sta on None), jotka toisen haaran _-malli ilmaisee, haluamme sallia new_setting_value:n tulla setting_value:ksi.

Voimme myös käyttää alaviivoja useissa paikoissa yhdessä mallissa jättääksemme huomiotta tiettyjä arvoja. Listaus 19-19 näyttää esimerkin toisen ja neljännen arvon huomiotta jättämisestä viiden alkion tuplesta.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-19/src/main.rs:here}}
}
Listing 19-19: Useiden tuple-osien huomiotta jättäminen

Tämä koodi tulostaa Some numbers: 2, 8, 32, ja arvot 4 ja 16 jätetään huomiotta.

Käyttämätön muuttuja aloittamalla sen nimi alaviivalla

Jos luot muuttujan mutta et käytä sitä missään, Rust antaa yleensä varoituksen, koska käyttämätön muuttuja voi olla bugi. Joskus on kuitenkin hyödyllistä pystyä luomaan muuttuja, jota et vielä käytä, esimerkiksi prototyyppiessasi tai aloittaessasi projektia. Tässä tilanteessa voit kertoa Rustille, ettei sen tarvitse varoittaa käyttämättömästä muuttujasta aloittamalla muuttujan nimen alaviivalla. Listauksessa 19-20 luomme kaksi käyttämätöntä muuttujaa, mutta kun käännämme tämän koodin, meidän pitäisi saada varoitus vain yhdestä niistä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-20/src/main.rs}}
}
Listing 19-20: Muuttujan nimen aloittaminen alaviivalla käyttämättömän muuttujan varoituksen välttämiseksi

Tässä saamme varoituksen siitä, ettei muuttujaa y käytetä, mutta emme saa varoitusta siitä, ettei _x:ää käytetä.

Huomaa, että pelkän _:n käytön ja alaviivalla alkavan nimen käytön välillä on hienovarainen ero. Syntaksi _x sitoo silti arvon muuttujaan, kun taas _ ei sido lainkaan. Osoittaaksemme tapauksen, jossa tämä ero on tärkeä, listaus 19-21 antaa meille virheen.

{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-21/src/main.rs:here}}
Listing 19-21: Alaviivalla alkava käyttämätön muuttuja sitoo silti arvon, mikä saattaa ottaa arvon omistajuuden

Saamme virheen, koska s-arvo siirretään silti _s:ään, mikä estää meitä käyttämästä s:ää uudelleen. Pelkän alaviivan käyttö ei kuitenkaan koskaan sido arvoon. Listaus 19-22 kääntyy ilman virheitä, koska s:ää ei siirretä _:ään.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-22/src/main.rs:here}}
}
Listing 19-22: Alaviivan käyttö ei sido arvoa

Tämä koodi toimii hyvin, koska emme koskaan sido s:ää mihinkään; sitä ei siirretä.

Arvon jäljellä olevat osat ..-syntaksilla

Arvoilla, joilla on monta osaa, voimme käyttää ..-syntaksia käyttääksemme tiettyjä osia ja jättääksemme loput huomiotta, välttäen tarpeen listata alaviivoja jokaiselle huomiotta jätetylle arvolle. ..-malli jättää huomiotta kaikki arvon osat, joita emme ole eksplisiittisesti vastanneet mallin lopussa. Listauksessa 19-23 meillä on Point-rakenne, joka sisältää koordinaatin kolmiulotteisessa avaruudessa. match-lausekkeessa haluamme käsitellä vain x-koordinaattia ja jättää huomiotta y- ja z-kenttien arvot.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-23/src/main.rs:here}}
}
Listing 19-23: Kaikkien Point-rakenteen kenttien huomiotta jättäminen paitsi x käyttämällä ..-syntaksia

Listaamme x-arvon ja sisällytämme sitten vain ..-mallin. Tämä on nopeampaa kuin joutua listaamaan y: _ ja z: _, varsinkin kun työskentelemme rakenteiden kanssa, joilla on paljon kenttiä tilanteissa, joissa vain yksi tai kaksi kenttää on relevantteja.

Syntaksi .. laajenee niin moneen arvoon kuin tarvitaan. Listaus 19-24 näyttää, miten käyttää ..-syntaksia tuplen kanssa.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-24/src/main.rs}}
}
Listing 19-24: Vain ensimmäisen ja viimeisen arvon vastaaminen tuplesta ja kaikkien muiden arvojen huomiotta jättäminen

Tässä koodissa ensimmäinen ja viimeinen arvo vastataan first:llä ja last:lla. .. vastaa ja jättää huomiotta kaiken keskeltä.

..-syntaksin käytön täytyy kuitenkin olla yksiselitteistä. Jos on epäselvää, mitkä arvot on tarkoitettu vastattaviksi ja mitkä jätettäväksi huomiotta, Rust antaa virheen. Listaus 19-25 näyttää esimerkin ..-syntaksin epäselvästä käytöstä, joten se ei käänny.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-25/src/main.rs}}
Listing 19-25: Yritys käyttää ..-syntaksia epäselvästi

Kun käännämme tämän esimerkin, saamme tämän virheen:

{{#include ../listings/ch19-patterns-and-matching/listing-19-25/output.txt}}

Rustille on mahdotonta määrittää, kuinka monta arvoa tuplesta jätetään huomiotta ennen kuin arvo vastataan second:lla ja kuinka monta arvoa jätetään huomiotta sen jälkeen. Tämä koodi voisi tarkoittaa, että haluamme jättää huomiotta 2:n, sitoa second:n arvoon 4 ja sitten jättää huomiotta 8:n, 16:n ja 32:n; tai että haluamme jättää huomiotta 2:n ja 4:n, sitoa second:n arvoon 8:n ja sitten jättää huomiotta 16:n ja 32:n; ja niin edelleen. Muuttujan nimi second ei tarkoita Rustille mitään erityistä, joten saamme kääntäjävirheen, koska ..-syntaksin käyttö kahdessa paikassa näin on epäselvää.

Lisäehtoja match-ehtojen avulla

Match-ehto on lisä-if-ehto, joka määritellään mallin jälkeen match-haarassa ja jonka täytyy myös täyttyä, jotta haara valitaan. Match-ehdot ovat hyödyllisiä ilmaisemaan monimutkaisempia ideoita kuin pelkkä malli sallii. Huomaa kuitenkin, että ne ovat saatavilla vain match-lausekkeissa, eivät if let- tai while let -lausekkeissa.

Ehto voi käyttää mallissa luotuja muuttujia. Listaus 19-26 näyttää match-lausekkeen, jossa ensimmäisellä haaralla on malli Some(x) ja match-ehto if x % 2 == 0 (joka on tosi, jos luku on parillinen).

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-26/src/main.rs:here}}
}
Listing 19-26: Match-ehdon lisääminen malliin

Tämä esimerkki tulostaa The number 4 is even. Kun num verrataan ensimmäisen haaran malliin, se vastaa, koska Some(4) vastaa Some(x):n. Sitten match-ehto tarkistaa, onko x:n jakojäännös kahdella jaolla 0, ja koska se on, ensimmäinen haara valitaan.

Jos num olisi ollut Some(5) sen sijaan, ensimmäisen haaran match-ehto olisi ollut epätosi, koska 5:n jakojäännös kahdella on 1, mikä ei ole yhtä suuri kuin 0. Rust siirtyisi sitten toiseen haaraan, joka vastaisi, koska toisella haaralla ei ole match-ehtoa ja se siksi vastaa minkä tahansa Some-variantin.

Ehtoa if x % 2 == 0 ei voi ilmaista mallissa, joten match-ehto antaa meille mahdollisuuden ilmaista tämän logiikan. Tämän lisäilmaisuvoiman haittapuoli on, että kääntäjä ei yritä tarkistaa tyhjentävyyttä, kun match-ehtolausekkeita on mukana.

Käsiteltäessä listaus 19-11 mainitsimme, että voisimme käyttää match-ehtoja ratkaistaksemme mallien varjostusongelmamme. Muista, että loimme uuden muuttujan match-lausekkeen malliin sen sijaan, että olisimme käyttäneet match-lausekkeen ulkopuolista muuttujaa. Tuo uusi muuttuja tarkoitti, ettei voitu testata ulomman muuttujan arvoa vastaan. Listaus 19-27 näyttää, miten voimme käyttää match-ehtoa korjataksemme tämän ongelman.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-27/src/main.rs}}
}
Listing 19-27: Match-ehdon käyttö ulomman muuttujan yhtäsuuruuden testaamiseen

Tämä koodi tulostaa nyt Default case, x = Some(5). Toisen match-haaran malli ei esittele uutta y-muuttujaa, joka varjostaisi ulomman y:n, mikä tarkoittaa, että voimme käyttää ulompaa y:tä match-ehdossa. Sen sijaan, että määrittelisimme mallin Some(y):ksi, mikä olisi varjostanut ulomman y:n, määrittelemme mallin Some(n):ksi. Tämä luo uuden n-muuttujan, joka ei varjosta mitään, koska n-muuttujaa ei ole match-lausekkeen ulkopuolella.

Match-ehto if n == y ei ole malli eikä siksi esittele uusia muuttujia. Tämä y on ulompi y eikä uusi y, joka varjostaisi sen, ja voimme etsiä arvoa, jolla on sama arvo kuin ulommalla y:llä vertaamalla n:tä y:hyn.

Voit myös käyttää tai-operaattoria |-merkintää match-ehdossa määrittääksesi useita malleja; match-ehtoehto koskee kaikkia malleja. Listaus 19-28 näyttää prioriteetin, kun yhdistetään |-operaattoria käyttävä malli match-ehtoon. Tämän esimerkin tärkeä osa on, että if y match-ehto koskee 4:ää, 5:tä ja 6:ta, vaikka saattaisi näyttää siltä, että if y koskee vain 6:ta.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-28/src/main.rs:here}}
}
Listing 19-28: Useiden mallien yhdistäminen match-ehtoon

Match-ehto sanoo, että haara vastaa vain, jos x:n arvo on yhtä suuri kuin 4, 5 tai 6 ja jos y on true. Kun tämä koodi suoritetaan, ensimmäisen haaran malli vastaa, koska x on 4, mutta match-ehto if y on epätosi, joten ensimmäistä haaraa ei valita. Koodi siirtyy toiseen haaraan, joka vastaa, ja tämä ohjelma tulostaa no. Syy on, että if-ehto koskee koko mallia 4 | 5 | 6, ei vain viimeistä arvoa 6. Toisin sanoen match-ehdon prioriteetti malliin nähden käyttäytyy näin:

(4 | 5 | 6) if y => ...

eikä näin:

4 | 5 | (6 if y) => ...

Kun koodi on suoritettu, prioriteetin käyttäytyminen on ilmeistä: jos match-ehto koskisi vain viimeistä arvoa |-operaattorilla määritellyssä arvolistassa, haara olisi vastannut ja ohjelma olisi tulostanut yes.

@-sidonnat

At-operaattori @ antaa meidän luoda muuttujan, joka pitää arvoa samalla kun testaamme arvoa mallin vastaamista varten. Listauksessa 19-29 haluamme testata, että Message::Hello-viestin id-kenttä on alueella 3..=7. Haluamme myös sitoa arvon muuttujaan id, jotta voimme käyttää sitä haaraan liittyvässä koodissa.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-patterns-and-matching/listing-19-29/src/main.rs:here}}
}
Listing 19-29: @:n käyttö arvon sitomiseen mallissa samalla kun sitä testataan

Tämä esimerkki tulostaa Found an id in range: 5. Määrittämällä id @ ennen aluetta 3..=7 kaappaamme minkä tahansa arvon, joka vastasi aluetta, muuttujaan nimeltä id samalla kun testaamme, että arvo vastasi aluemallia.

Toisella haaralla, jossa mallissa on vain alue määriteltynä, haaraan liittyvässä koodissa ei ole muuttujaa, joka sisältäisi id-kentän todellisen arvon. id-kentän arvo olisi voinut olla 10, 11 tai 12, mutta kyseiseen malliin liittyvä koodi ei tiedä, kumpi se on. Mallikoodi ei voi käyttää id-kentän arvoa, koska emme ole tallentaneet id-arvoa muuttujaan.

Viimeisellä haaralla, jossa olemme määritelleet muuttujan ilman aluetta, meillä on arvo käytettävissä haaran koodissa muuttujassa nimeltä id. Syy on, että olemme käyttäneet rakenteen kentän oikotiesyntaksia. Mutta emme ole soveltaneet mitään testiä id-kentän arvolle tässä haarassa, kuten teimme kahdella ensimmäisellä haaralla: mikä tahansa arvo vastaisi tätä mallia.

@-operaattorin avulla voimme testata arvoa ja tallentaa sen muuttujaan yhdessä mallissa.

Yhteenveto

Rustin mallit ovat hyvin hyödyllisiä erilaisten datatyyppien erottelussa. Kun niitä käytetään match-lausekkeissa, Rust varmistaa, että mallisi kattavat jokaisen mahdollisen arvon, tai ohjelmasi ei käänny. Mallit let-lausekkeissa ja funktioiden parametreissa tekevät näistä rakenteista hyödyllisempiä mahdollistamalla arvojen purkamisen pienempiin osiin ja näiden osien osoittamisen muuttujiin. Voimme luoda yksinkertaisia tai monimutkaisia malleja tarpeidemme mukaan.

Seuraavaksi, kirjan toiseksi viimeisessä luvussa, tarkastelemme Rustin eri ominaisuuksien edistyneitä puolia.

Kehittyneet Rustin ominaisuudet

Tässä luvussa tarkastelemme Rustin kehittyneempiä ominaisuuksia, joita ei välttämättä tarvita jokapäiväisessä ohjelmoinnissa, mutta jotka voivat olla hyödyllisiä erityisissä tilanteissa. Näihin kuuluvat:

  • Tyyppijärjestelmän edistyneet ominaisuudet, kuten assosioidut tyypit ja kehittyneet trait-mallit.
  • Unsafe Rust, joka antaa suoremman pääsyn muistiin ja mahdollistaa alhaisen tason ohjelmoinnin.
  • Makrot ja niiden hyödyntäminen, mukaan lukien proseduraaliset makrot.

Vaikka nämä ominaisuudet eivät ole välttämättömiä Rustin peruskäytössä, niiden ymmärtäminen voi auttaa optimoimaan ja laajentamaan Rust-sovelluksia entistä tehokkaammin.

Seuraavaksi tarkastelemme kehittyneitä trait-ominaisuuksia ja Rustin tehokasta tyyppijärjestelmää.

Turvaton Rust

Unsafe Rust

Kaikki koodi, josta olemme tähän asti puhuneet, on noudattanut Rustin muistiturvallisuustakuita käännösaikana. Rustissa on kuitenkin toinen, piilossa oleva kieli, joka ei pakota näitä muistiturvallisuustakuita: sitä kutsutaan unsafe Rustiksi, ja se toimii kuten tavallinen Rust, mutta antaa meille ylimääräisiä supervoimia.

Unsafe Rust on olemassa, koska luonteeltaan staattinen analyysi on konservatiivista. Kun kääntäjä yrittää selvittää, pitääkö koodi takuista kiinni, on parempi hylätä joitakin kelvollisia ohjelmia kuin hyväksyä joitakin virheellisiä ohjelmia. Vaikka koodi saattaisi olla kunnossa, jos Rust-kääntäjällä ei ole tarpeeksi tietoa olla varma, se hylkää koodin. Näissä tapauksissa voit käyttää unsafe-koodia kertoaksesi kääntäjälle: ”Luota minuun, tiedän mitä teen.” Varo kuitenkin, että käytät unsafe Rustia omalla vastuullasi: jos käytät unsafe-koodia väärin, voi syntyä ongelmia muistiturvattomuuden vuoksi, kuten null-osoittimen dereferoinnin seurauksena.

Toinen syy sille, miksi Rustilla on unsafe-alter ego, on se, että taustalla oleva tietokonelaitteisto on luonnostaan turvaton. Jos Rust ei sallisi unsafe-operaatioita, et voisi tehdä tiettyjä tehtäviä. Rustin täytyy sallia matalan tason järjestelmäohjelmointi, kuten suora vuorovaikutus käyttöjärjestelmän kanssa tai jopa oman käyttöjärjestelmän kirjoittaminen. Matalan tason järjestelmäohjelmointi on yksi kielen tavoitteista. Tutkitaan, mitä unsafe Rustilla voi tehdä ja miten se tehdään.

Unsafe-supervoimien käyttö

Siirtyäksesi unsafe Rustiin, käytä unsafe-avainsanaa ja aloita uusi lohko, joka sisältää unsafe-koodin. Unsafe Rustissa voit tehdä viisi asiaa, joita et voi tehdä safe Rustissa; kutsumme niitä unsafe-supervoimiksi. Näihin supervoimiin kuuluu kyky:

  1. Dereferoida raakaosoitin.
  2. Kutsua unsafe-funktiota tai -metodia.
  3. Käyttää tai muokata muuttuvaa staattista muuttujaa.
  4. Toteuttaa unsafe-trait.
  5. Käyttää union-tyyppien kenttiä.

On tärkeää ymmärtää, että unsafe ei poista lainauskääntäjää eikä mitään Rustin muista turvatarkistuksista: jos käytät viitettä unsafe-koodissa, sitä tarkistetaan silti. unsafe-avainsana antaa pääsyn vain näihin viiteen ominaisuuteen, joita kääntäjä ei sitten tarkista muistiturvallisuuden osalta. Saat silti jonkin verran turvallisuutta unsafe-lohkon sisällä.

Lisäksi unsafe ei tarkoita, että lohkon sisällä oleva koodi olisi välttämättä vaarallista tai että siinä olisi varmasti muistiturvallisuusongelmia: tarkoitus on, että ohjelmoijana varmistat, että unsafe-lohkon sisällä oleva koodi käyttää muistia kelvollisella tavalla.

Ihmiset tekevät virheitä, mutta vaatimalla näiden viiden unsafe-operaation olevan unsafe-merkityissä lohkoissa tiedät, että muistiturvallisuuteen liittyvät virheet ovat unsafe-lohkossa. Pidä unsafe-lohkot pieninä; olet kiitollinen myöhemmin, kun tutkit muistivirheitä.

Erottaaksesi unsafe-koodin mahdollisimman paljon, on parasta sulkea se safe-abstraktioon ja tarjota safe-rajapinta; puhumme tästä myöhemmin luvussa, kun tarkastelemme unsafe-funktioita ja -metodeja. Osa standardikirjastosta on toteutettu safe-abstraktioina auditoidun unsafe-koodin päälle. Unsafe-koodin kääriminen safe-abstraktioon estää unsafe-käytön leviämästä kaikkiin paikkoihin, joissa sinä tai käyttäjäsi haluatte käyttää unsafe-koodilla toteutettua toiminnallisuutta, koska safe-abstraktion käyttö on turvallista.

Katsotaan kukin viidestä unsafe-supervoimasta vuorollaan. Tarkastelemme myös abstraktioita, jotka tarjoavat turvallisen rajapinnan unsafe-koodille.

Raakaosoittimen dereferointi

Luvussa 4 ”Ripustuvat viitteet”-kohdassa mainitsimme, että kääntäjä varmistaa viitteiden olevan aina kelvollisia. Unsafe Rustissa on kaksi uutta tyyppiä, raakaosoittimet, jotka muistuttavat viitteitä. Kuten viitteillä, raakaosoittimet voivat olla muuttumattomia tai muuttuvia, ja ne kirjoitetaan muodossa *const T ja *mut T. Asteriski ei ole dereferointioperaattori; se on osa tyyppinimeä. Raakaosoittimien kontekstissa muuttumaton tarkoittaa, että osoitinta ei voi suoraan sijoittaa uudelleen dereferoinnin jälkeen.

Toisin kuin viitteet ja älyosoittimet, raakaosoittimet:

  • Voivat sivuuttaa lainaussäännöt, kun samassa paikassa on sekä muuttumaton että muuttuva osoitin tai useita muuttuvia osoittimia
  • Eivät takaa osoittavan kelvollista muistia
  • Voivat olla null
  • Eivät toteuta automaattista siivousta

Luopumalla siitä, että Rust pakottaa nämä takuut, voit luopua taatusta turvallisuudesta vastineeksi paremmasta suorituskyvystä tai kyvystä käyttää toista kieltä tai laitteistoa, joissa Rustin takuut eivät päde.

Listaus 20-1 näyttää, miten luodaan muuttumaton ja muuttuva raakaosoitin.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-01/src/main.rs:here}}
}
Listing 20-1: Raakaosoittimien luominen raakalainaajaoperaattoreilla

Huomaa, ettemme sisällytä unsafe-avainsanaa tähän koodiin. Raakaosoittimia voi luoda safe-koodissa; emme vain voi dereferoida raakaosoittimia unsafe-lohkon ulkopuolella, kuten näet pian.

Loimme raakaosoittimet raakalainaajaoperaattoreilla: &raw const num luo muuttumattoman raakaosoittimen *const i32, ja &raw mut num luo muuttuvan raakaosoittimen *mut i32. Koska loimme ne suoraan paikallisesta muuttujasta, tiedämme näiden raakaosoittimien olevan kelvollisia, mutta emme voi olettaa samaa mistä tahansa raakaosoittimesta.

Todistaaksemme tämän luomme seuraavaksi raakaosoittimen, jonka kelvollisuudesta emme ole yhtä varmoja, käyttämällä as-avainsanaa arvon tyypinmuunnokseen raakalainaajaoperaattorin sijaan. Listaus 20-2 näyttää, miten luodaan raakaosoitin mielivalpaiseen muistipaikkaan. Mielivaltaisen muistin käyttö on määrittelemätöntä käyttäytymistä: osoitteessa voi olla dataa tai ei, kääntäjä voi optimoida koodin niin ettei muistia käytetä lainkaan, tai ohjelma voi päättyä segmentation faultiin. Yleensä tällaiselle koodille ei ole hyvää syytä, varsinkaan kun voit käyttää raakalainaajaoperaattoria, mutta se on mahdollista.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-02/src/main.rs:here}}
}
Listing 20-2: Raakaosoittimen luominen mielivaltaiseen muistiosoitteeseen

Muista, että raakaosoittimia voi luoda safe-koodissa, mutta emme voi dereferoida raakaosoittimia ja lukea osoitettua dataa. Listauksessa 20-3 käytämme dereferointioperaattoria * raakaosoittimelle, mikä vaatii unsafe-lohkon.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-03/src/main.rs:here}}
}
Listing 20-3: Raakaosoittimien dereferointi unsafe-lohkossa

Osoittimen luominen ei tee haittaa; ongelma syntyy vasta, kun yritämme käyttää osoittimen arvoa, jolloin voimme joutua käsittelemään virheellistä arvoa.

Huomaa myös, että listauksissa 20-1 ja 20-3 loimme *const i32- ja *mut i32 -raakaosoittimet, jotka molemmat osoittivat samaan muistipaikkaan, jossa num on tallennettuna. Jos yrittäisimme luoda muuttumattoman ja muuttuvan viitteen num-muuttujaan, koodi ei kääntyisi, koska Rustin omistussäännöt eivät salli muuttuvaa viitettä samanaikaisesti muuttumattomien viitteiden kanssa. Raakaosoittimilla voimme luoda muuttuvan ja muuttumattoman osoittimen samaan paikkaan ja muuttaa dataa muuttuvan osoittimen kautta, mikä voi synnyttää datakilpailun. Ole varovainen!

Kaikkien näiden vaarojen jälkeen, miksi käyttäisit raakaosoittimia? Yksi tärkeä käyttötapaus on vuorovaikutus C-koodin kanssa, kuten näet seuraavassa osiossa. Toinen tapaus on safe-abstraktioiden rakentaminen, joita lainauskääntäjä ei ymmärrä. Esittelemme unsafe-funktiot ja katsomme esimerkin safe-abstraktiosta, joka käyttää unsafe-koodia.

Unsafe-funktion tai -metodin kutsuminen

Toinen operaatio, jonka voit tehdä unsafe-lohkossa, on unsafe-funktioiden kutsuminen. Unsafe-funktiot ja -metodit näyttävät täsmälleen tavallisilta funktioilta ja -metodeilta, mutta niillä on ylimääräinen unsafe ennen määritelmän loppuosaa. Tässä kontekstissa unsafe tarkoittaa, että funktiolla on vaatimuksia, jotka meidän täytyy täyttää kutsuessamme sitä, koska Rust ei voi taata niiden täyttymistä. Kutsumalla unsafe-funktiota unsafe-lohkossa sanomme, että olemme lukeneet funktion dokumentaation ja otamme vastuun sen sopimusten täyttämisestä.

Tässä on unsafe-funktio nimeltä dangerous, joka ei tee rungossaan mitään:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-01-unsafe-fn/src/main.rs:here}}
}

Meidän täytyy kutsua dangerous-funktiota erillisessä unsafe-lohkossa. Jos yritämme kutsua dangerous-funktiota ilman unsafe-lohkoa, saamme virheen:

{{#include ../listings/ch20-advanced-features/output-only-01-missing-unsafe/output.txt}}

unsafe-lohkossa vakuutamme Rustille, että olemme lukeneet funktion dokumentaation, ymmärrämme sen käytön ja olemme varmistaneet täyttävämme funktion sopimuksen.

Suorittaaksesi unsafe-operaatioita unsafe-funktion rungossa, sinun täytyy silti käyttää unsafe-lohkoa, kuten tavallisessa funktiossa, ja kääntäjä varoittaa, jos unohdat. Tämä auttaa pitämään unsafe-lohkot mahdollisimman pieninä, koska unsafe-operaatioita ei välttämättä tarvita koko funktion rungossa.

Turvallinen abstraktio unsafe-koodin päälle

Pelkästään se, että funktio sisältää unsafe-koodia, ei tarkoita, että koko funktio täytyy merkitä unsafeksi. Unsafe-koodin kääriminen safe-funktioon on itse asiassa yleinen abstraktio. Esimerkkinä tutkitaan standardikirjaston split_at_mut-funktiota, joka vaatii unsafe-koodia. Tutkimme, miten sen voisi toteuttaa. Tämä safe-metodi on määritelty muuttuville viipaleille: se ottaa yhden viipaleen ja jakaa sen kahteen annetun indeksin kohdalta. Listaus 20-4 näyttää split_at_mut-funktion käytön.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-04/src/main.rs:here}}
}
Listing 20-4: Turvallisen split_at_mut-funktion käyttö

Emme voi toteuttaa tätä funktiota pelkällä safe Rustilla. Yritys voisi näyttää listaukselta 20-5, joka ei kääntyisi. Yksinkertaisuuden vuoksi toteutamme split_at_mut-funktion funktiona eikä metodina ja vain i32-viipaleille eikä geneeriselle tyypille T.

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-05/src/main.rs:here}}
Listing 20-5: Yritys toteuttaa split_at_mut pelkällä safe Rustilla

Funktio hakee ensin viipaleen kokonaispituuden. Sitten se varmistaa, että parametrina annettu indeksi on viipaleen sisällä tarkistamalla, onko se pienempi tai yhtä suuri kuin pituus. Väite tarkoittaa, että jos annamme indeksin, joka on suurempi kuin pituus, funktio panikoi ennen kuin yrittää käyttää sitä.

Sitten palautamme kaksi muuttuvaa viipaletta monikkona: toinen alkuperäisen viipaleen alusta mid-indeksiin ja toinen mid-indeksistä viipaleen loppuun.

Kun yritämme kääntää listauksen 20-5 koodin, saamme virheen:

{{#include ../listings/ch20-advanced-features/listing-20-05/output.txt}}

Rustin lainauskääntäjä ei ymmärrä, että lainaamme viipaleen eri osia; se tietää vain, että lainaamme samaa viipaletta kahdesti. Viipaleen eri osien lainaaminen on periaatteessa ok, koska viipaleet eivät ole päällekkäin, mutta Rust ei ole tarpeeksi älykäs tietääkseen tämän. Kun tiedämme koodin olevan ok, mutta Rust ei, on aika turvautua unsafe-koodiin.

Listaus 20-6 näyttää, miten unsafe-lohkoa, raakaosoitinta ja unsafe-funktiokutsuja käytetään split_at_mut-toteutuksessa.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-06/src/main.rs:here}}
}
Listing 20-6: Unsafe-koodin käyttö split_at_mut-funktion toteutuksessa

Muista luvun 4 ”Viipaletyyppi” -kohdasta, että viipale on osoitin dataan ja viipaleen pituus. Käytämme len-metodia viipaleen pituuden hakemiseen ja as_mut_ptr-metodia viipaleen raakaosoittimen käyttöön. Tässä tapauksessa, koska meillä on muuttuva viipale i32-arvoille, as_mut_ptr palauttaa raakaosoittimen tyypillä *mut i32, jonka tallensimme muuttujaan ptr.

Pidämme väitteen, että mid-indeksi on viipaleen sisällä. Sitten tulee unsafe-koodi: slice::from_raw_parts_mut-funktio ottaa raakaosoittimen ja pituuden ja luo viipaleen. Käytämme sitä luomaan viipaleen, joka alkaa ptr:stä ja on mid kohdetta pitkä. Sitten kutsumme add-metodia ptr:llä argumenttina mid saadaksemme raakaosoittimen, joka alkaa kohdasta mid, ja luomme viipaleen tuolla osoittimella ja jäljellä olevien kohteiden määrällä mid:n jälkeen.

Funktio slice::from_raw_parts_mut on unsafe, koska se ottaa raakaosoittimen ja täytyy luottaa osoittimen kelvollisuuteen. Raakaosoittimien add-metodi on myös unsafe, koska sen täytyy luottaa siirtymän kohdeosoitteen kelvollisuuteen. Siksi jouduimme laittamaan unsafe-lohkon slice::from_raw_parts_mut- ja add-kutsujen ympärille. Koodia ja väitettä, että mid on pienempi tai yhtä suuri kuin len, tarkastelemalla voimme päätellä, että kaikki unsafe-lohkossa käytetyt raakaosoittimet osoittavat kelvollista dataa viipaleen sisällä. Tämä on hyväksyttävä ja asianmukainen unsafe-käyttö.

Huomaa, ettemme tarvitse merkitä tuloksena olevaa split_at_mut-funktiota unsafe-funktioksi, ja voimme kutsua sitä safe Rustista. Olemme luoneet safe-abstraktion unsafe-koodille toteutuksella, joka käyttää unsafe-koodia turvallisesti, koska se luo vain kelvollisia osoittimia datasta, johon funktiolla on pääsy.

Vastakohtana listauksen 20-7 slice::from_raw_parts_mut-käyttö todennäköisesti kaataa ohjelman, kun viipaletta käytetään. Tämä koodi ottaa mielivaltaisen muistipaikan ja luo 10 000 kohdetta pitkän viipaleen.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-07/src/main.rs:here}}
}
Listing 20-7: Viipaleen luominen mielivaltaisesta muistipaikasta

Emme omista muistia tässä mielivaltaisessa paikassa, eikä ole takuuta, että luotu viipale sisältää kelvollisia i32-arvoja. values-viipaleen käyttäminen kuin se olisi kelvollinen viipale johtaa määrittelemättömään käyttäytymiseen.

extern-funktioiden käyttö ulkoisen koodin kutsumiseen

Joskus Rust-koodisi täytyy vuorovaikuttaa toisella kielellä kirjoitetun koodin kanssa. Tätä varten Rustissa on extern-avainsana, joka helpottaa ulkomaisten funktioiden rajapinnan (FFI) luomista ja käyttöä; FFI on tapa, jolla ohjelmointikieli määrittelee funktioita ja sallii toisen (ulkomaisen) ohjelmointikielen kutsua niitä.

Listaus 20-8 näyttää, miten integroidaan C-standardikirjaston abs-funktio. extern-lohkoissa määriteltyjä funktioita on yleensä unsafe kutsua Rust-koodista, joten extern-lohkot täytyy myös merkitä unsafe-lohkoiksi. Syy on, että muut kielet eivät pakota Rustin sääntöjä ja takuita, eikä Rust voi tarkistaa niitä, joten turvallisuus on ohjelmoijan vastuulla.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-08/src/main.rs}}
}
Listing 20-8: Toisella kielellä määritellyn extern-funktion julistaminen ja kutsuminen

unsafe extern "C"-lohkossa listaamme ulkoisten funktioiden nimet ja signatuurit, joita haluamme kutsua. "C"-osa määrittää, mitä sovellusbinaarirajapintaa (ABI) ulkoinen funktio käyttää: ABI määrittää, miten funktiota kutsutaan assembly-tasolla. "C"-ABI on yleisin ja noudattaa C-ohjelmointikielen ABI:a. Tietoa kaikista Rustin tukemista ABI:sta on Rustin viitteessä.

Jokainen unsafe extern-lohkossa julistettu kohde on implisiittisesti unsafe. Jotkin FFI-funktiot ovat kuitenkin turvallisia kutsua. Esimerkiksi C-standardikirjaston abs-funktiolla ei ole muistiturvallisuusnäkökohtia, ja tiedämme sen toimivan millä tahansa i32:lla. Tällaisissa tapauksissa voimme käyttää safe-avainsanaa sanoaksemme, että tämä funktio on turvallinen kutsua vaikka se on unsafe extern-lohkossa. Kun teemme tämän muutoksen, sen kutsuminen ei enää vaadi unsafe-lohkoa, kuten listauksessa 20-9.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-09/src/main.rs}}
}
Listing 20-9: Funktion eksplisiittinen merkitseminen safe-funktioksi unsafe extern-lohkossa ja sen turvallinen kutsuminen

Funktion merkitseminen safe-funktioksi ei itsessään tee siitä turvallista! Sen sijaan se on lupaus Rustille, että funktio on turvallinen. Sinun vastuullasi on edelleen varmistaa, että lupaus pitää!

Rust-funktioiden kutsuminen muista kielistä

Voimme myös käyttää extern-avainsanaa luodaksemme rajapinnan, jonka avulla muut kielet voivat kutsua Rust-funktioita. Sen sijaan, että luomme koko extern-lohkon, lisäämme extern-avainsanan ja määrittelemme käytettävän ABI:n juuri ennen fn-avainsanaa kyseiselle funktiolle. Lisäämme myös #[unsafe(no_mangle)]-annotaation kertoaksemme Rust-kääntäjälle, ettei sen pidä manglata tämän funktion nimeä. Manglaus tarkoittaa, että kääntäjä muuttaa funktiolle antamamme nimen toiseksi, informatiivisemmaksi mutta vähemmän luettavaksi nimellä, jota muut käännösprosessin osat käyttävät. Jokainen ohjelmointikielen kääntäjä manglaa nimiä hieman eri tavalla, joten jotta Rust-funktiota voisi kutsua muista kielistä, meidän täytyy poistaa Rust-kääntäjän nimen manglaus. Tämä on unsafe, koska ilman sisäänrakennettua manglausta voi syntyä nimikolisioneita kirjastojen välillä, joten vastuullamme on varmistaa, että valitsemamme nimi on turvallinen viedä ilman manglausta.

Seuraavassa esimerkissä teemme call_from_c-funktion käytettäväksi C-koodista sen jälkeen, kun se on käännetty jaetulla kirjastolla ja linkitetty C:stä:

#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

Tämä extern-käyttö vaatii unsafe-merkinnän vain attribuutissa, ei extern-lohkossa.

Muuttuvan staattisen muuttujan käyttö tai muokkaus

Tässä kirjassa emme ole vielä puhuneet globaaleista muuttujista, joita Rust tukee mutta jotka voivat olla ongelmallisia Rustin omistussääntöjen kanssa. Jos kaksi säiettä käyttää samaa muuttuvaa globaalia muuttujaa, voi syntyä datakilpailu.

Rustissa globaaleja muuttujia kutsutaan staattisiksi muuttujiksi. Listaus 20-10 näyttää esimerkin staattisen muuttujan julistamisesta ja käytöstä, jonka arvo on merkkijonoviipale.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-10/src/main.rs}}
}
Listing 20-10: Muuttumattoman staattisen muuttujan määrittely ja käyttö

Staattiset muuttujat muistuttavat vakioita, joista puhuimme luvun 3 ”Vakioiden julistaminen” -kohdassa. Staattisten muuttujien nimet ovat käytännön mukaan SCREAMING_SNAKE_CASE-muodossa. Staattiset muuttujat voivat tallentaa vain viitteitä 'static-elinaikaisella, mikä tarkoittaa, että Rust-kääntäjä voi päätellä elinaikaisuuden emmekä joudu merkitsemään sitä eksplisiittisesti. Muuttumattoman staattisen muuttujan käyttö on turvallista.

Hienovarainen ero vakioiden ja muuttumattomien staattisten muuttujien välillä on, että staattisen muuttujan arvolla on kiinteä osoite muistissa. Arvon käyttö käyttää aina samaa dataa. Vakioilla sen sijaan dataa saa monistaa aina kun niitä käytetään. Toinen ero on, että staattiset muuttujat voivat olla muuttuvia. Muuttuvan staattisen muuttujan käyttö ja muokkaus on unsafe. Listaus 20-11 näyttää, miten julistetaan, käytetään ja muokataan muuttuvaa staattista muuttujaa nimeltä COUNTER.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-11/src/main.rs}}
}
Listing 20-11: Muuttuvasta staattisesta muuttujasta lukeminen tai siihen kirjoittaminen on unsafe.

Kuten tavallisilla muuttujilla, määrittelemme muuttuvuuden mut-avainsanalla. Kaiken COUNTER-muuttujaa lukevan tai kirjoittavan koodin täytyy olla unsafe-lohkossa. Listauksen 20-11 koodi kääntyy ja tulostaa COUNTER: 3 odotetusti, koska se on yksisäikeinen. Usean säikeen käyttö COUNTER-muuttujalle johtaisi todennäköisesti datakilpailuihin, joten se on määrittelemätöntä käyttäytymistä. Siksi meidän täytyy merkitä koko funktio unsafe-funktioksi ja dokumentoida turvallisuusrajoitus, jotta kutsuja tietää, mitä saa ja ei saa tehdä turvallisesti.

Kun kirjoitamme unsafe-funktion, on idiomaattista kirjoittaa kommentti, joka alkaa SAFETY-sanalla ja selittää, mitä kutsujan täytyy tehdä kutsuakseen funktion turvallisesti. Vastaavasti kun suoritamme unsafe-operaation, on idiomaattista kirjoittaa kommentti, joka alkaa SAFETY-sanalla ja selittää, miten turvallisuussäännöt täyttyvät.

Lisäksi kääntäjä estää oletuksena yritykset luoda viitteitä muuttuvaan staattiseen muuttujaan kääntäjän lintin kautta. Sinun täytyy joko eksplisiittisesti poistua lintin suojauksesta lisäämällä #[allow(static_mut_refs)]-annotaatio tai käyttää muuttuvaa staattista muuttujaa raakaosoittimen kautta, joka on luotu raakalainaajaoperaattoreilla. Tämä koskee myös tapauksia, joissa viite luodaan näkymättömästi, kuten tämän listauksen println!-makrossa. Vaatimus, että viitteet muuttuviin staattisiin muuttujiin luodaan raakaosoittimien kautta, auttaa tekemään niiden käytön turvallisuusvaatimukset selkeämmiksi.

Globaalisti saatavilla olevan muuttuvan datan kanssa on vaikea varmistaa, ettei datakilpailuja synny, minkä vuoksi Rust pitää muuttuvia staattisia muuttujia unsafeina. Mahdollisuuksien mukaan on parempi käyttää luvussa 16 käsiteltyjä rinnakkaisuustekniikoita ja säieturvallisia älyosoittimia, jotta kääntäjä tarkistaa eri säikeiden datan käytön turvallisuuden.

Unsafe-traitin toteuttaminen

Voimme käyttää unsafe-avainsanaa toteuttaaksemme unsafe-traitin. Trait on unsafe, kun ainakin yhdellä sen metodeista on invariantti, jota kääntäjä ei voi varmistaa. Julistamme traitin unsafe-traitiksi lisäämällä unsafe-avainsanan ennen trait-sanaa ja merkitsemme traitin toteutuksen myös unsafeksi, kuten listauksessa 20-12.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-12/src/main.rs:here}}
}
Listing 20-12: Unsafe-traitin määrittely ja toteutus

Käyttämällä unsafe impl lupaamme pitää kiinni invarianteista, joita kääntäjä ei voi varmistaa.

Esimerkkinä muista luvun 16 ”Laajennettava rinnakkaisuus Send- ja Sync-traiteilla” -kohdasta Send- ja Sync-merkki-traitit: kääntäjä toteuttaa nämä traitit automaattisesti, jos tyypit koostuvat kokonaan muista tyypeistä, jotka toteuttavat Send- ja Sync-traitit. Jos toteutamme tyypin, joka sisältää tyypin, joka ei toteuta Send- tai Sync-traitia, kuten raakaosoittimia, ja haluamme merkitä tyypin Send- tai Sync-tyypiksi, meidän täytyy käyttää unsafe-avainsanaa. Rust ei voi varmistaa, että tyypimme täyttää takuut siitä, että sen voi lähettää säikeiden välillä tai käyttää useasta säikeestä; siksi meidän täytyy tehdä tarkistukset manuaalisesti ja ilmaista se unsafe-avainsanalla.

Union-tyypin kenttien käyttö

Viimeinen toiminto, joka toimii vain unsafe-koodissa, on union-tyypin kenttien käyttö. Union muistuttaa struct-rakennetta, mutta vain yhtä julistettua kenttää käytetään tietyssä instanssissa kerrallaan. Unioneja käytetään pääasiassa C-koodin unionien kanssa vuorovaikutukseen. Union-kenttien käyttö on unsafe, koska Rust ei voi taata, minkä tyyppistä dataa union-instanssissa on tällä hetkellä tallennettuna. Lisätietoa unioneista on Rustin viitteessä.

Miri unsafe-koodin tarkistamiseen

Kun kirjoitat unsafe-koodia, saatat haluta varmistaa, että kirjoittamasi on todella turvallista ja oikein. Yksi parhaista tavoista on käyttää Miriä, virallista Rust-työkalua määrittelemättömän käyttäytymisen havaitsemiseen. Lainauskääntäjä on staattinen työkalu, joka toimii käännösaikana, kun taas Miri on dynaaminen työkalu, joka toimii ajonaikana. Se tarkistaa koodin ajamalla ohjelman tai sen testisarjan ja havaitsemalla, kun rikot sääntöjä, joita se ymmärtää Rustin toiminnasta.

Miri vaatii Rustin yöversion (josta puhumme lisää liitteessä G: Miten Rust tehdään ja ”yö-Rust”). Voit asentaa sekä Rustin yöversion että Miri-työkalun komennolla rustup +nightly component add miri. Tämä ei muuta projektisi käyttämää Rust-versiota; se vain lisää työkalun järjestelmääsi käytettäväksi tarvittaessa. Voit ajaa Miriä projektilla komennoilla cargo +nightly miri run tai cargo +nightly miri test.

Esimerkkinä siitä, kuinka hyödyllinen tämä voi olla, katsotaan mitä tapahtuu, kun ajamme sen listauksen 20-7 koodilla.

{{#include ../listings/ch20-advanced-features/listing-20-07/output.txt}}

Miri varoittaa oikein, että muunnamme kokonaisluvun osoittimeksi, mikä voi olla ongelma, mutta Miri ei voi päätellä, onko ongelmaa, koska se ei tiedä osoittimen alkuperää. Sitten Miri palauttaa virheen, koska listauksessa 20-7 on määrittelemätöntä käyttäytymistä ripustuvan osoittimen vuoksi. Mirin ansiosta tiedämme nyt, että määrittelemättömän käyttäytymisen riski on olemassa, ja voimme miettiä, miten koodin saa turvalliseksi. Joissakin tapauksissa Miri voi jopa ehdottaa virheiden korjaamista.

Miri ei havaitse kaikkea, mitä unsafe-koodissa voi mennä pieleen. Miri on dynaaminen analyysityökalu, joten se havaitsee vain ongelmat koodissa, joka todella ajetaan. Tämä tarkoittaa, että sinun täytyy käyttää sitä yhdessä hyvien testaustekniikoiden kanssa lisätäksesi luottamusta kirjoittamaasi unsafe-koodiin. Miri ei myöskään kata kaikkia mahdollisia tapoja, joilla koodi voi olla epäluotettavaa.

Toisin sanottuna: jos Miri havaitsee ongelman, tiedät että siellä on bugi, mutta se, että Miri ei havaitse bugia, ei tarkoita ettei ongelmaa olisi. Se voi kuitenkin havaita paljon. Kokeile ajaa sitä tämän luvun muilla unsafe-esimerkeillä ja katso mitä se sanoo!

Lisätietoa Miristä on sen GitHub-repositoriossa.

Unsafe-koodin oikea käyttö

Yhden edellä käsitellyistä viidestä supervoimasta käyttäminen unsafe-avainsanalla ei ole väärin eikä edes paheksuttavaa, mutta unsafe-koodin saaminen oikein on vaikeampaa, koska kääntäjä ei voi auttaa muistiturvallisuuden ylläpidossa. Kun sinulla on syy käyttää unsafe-koodia, voit tehdä niin, ja eksplisiittinen unsafe-merkintä helpottaa ongelmien lähteen jäljittämistä. Aina kun kirjoitat unsafe-koodia, voit käyttää Miriä varmistaaksesi luottamuksesi siihen, että koodi noudattaa Rustin sääntöjä.

Paljon syvällisempää tietoa unsafe Rustin tehokkaasta käytöstä löydät Rustin virallisesta unsafe-oppaasta, The Rustonomiconista.

Kehittyneet traitit

Edistyneet traitit

Käsittelimme traitit ensimmäisen kerran luvun 10 ”Jaetun käyttäytymisen määrittely traitien avulla” -kohdassa, mutta emme käsitelleet edistyneempiä yksityiskohtia. Nyt kun tiedät Rustista enemmän, voimme syventyä yksityiskohtiin.

Traitien määrittely assosioituneilla tyypeillä

Assosioituneet tyypit yhdistävät tyyppipaikkamerkin traitiin siten, että traitin metodimääritelmissä voidaan käyttää näitä paikkamerkkityyppejä signatuureissa. Traitin toteuttaja määrittää konkreettisen tyypin paikkamerkkityypin sijaan tietylle toteutukselle. Näin voimme määritellä traitin, joka käyttää tyyppejä, tietämättä tarkalleen mitä tyyppejä ne ovat ennen traitin toteutusta.

Olemme kuvanneet useimmat tämän luvun edistyneet ominaisuudet harvoin tarvittaviksi. Assosioituneet tyypit ovat välimaastossa: niitä käytetään harvemmin kuin kirjan muissa osissa selitettyjä ominaisuuksia, mutta yleisemmin kuin monia tämän luvun muita ominaisuuksia.

Esimerkki traitista assosioituneella tyypillä on standardikirjaston tarjoama Iterator-trait. Assosioitu tyyppi on nimeltään Item ja edustaa niiden arvojen tyyppiä, joita Iterator-traitin toteuttava tyyppi iteroi. Iterator-traitin määritelmä on listauksessa 20-13.

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-13/src/lib.rs}}
Listing 20-13: Iterator-traitin määritelmä, jossa on assosioitu tyyppi Item

Tyyppi Item on paikkamerkki, ja next-metodin määritelmä näyttää, että se palauttaa arvoja tyypillä Option<Self::Item>. Iterator-traitin toteuttajat määrittävät konkreettisen tyypin Item-tyypille, ja next-metodi palauttaa Option-arvon, joka sisältää kyseisen konkreettisen tyypin arvon.

Assosioituneet tyypit saattavat vaikuttaa samankaltaiselta käsitteeltä kuin geneerisyys, koska geneerisyyden avulla voidaan määritellä funktio ilman tietoa siitä, mitä tyyppejä se käsittelee. Tarkastellaksemme näiden kahden käsitteen eroa, katsomme Iterator-traitin toteutusta tyypille nimeltä Counter, jossa Item-tyypiksi määritetään u32:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-22-iterator-on-counter/src/lib.rs:ch19}}

Tämä syntaksi näyttää verrattavissa geneerisyyden syntaksiin. Miksi emme siis määrittelisi Iterator-traitia geneerisesti, kuten listauksessa 20-14?

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-14/src/lib.rs}}
Listing 20-14: Hypoteettinen Iterator-traitin määritelmä geneerisyyttä käyttäen

Ero on siinä, että geneerisyyttä käytettäessä, kuten listauksessa 20-14, meidän täytyy merkitä tyypit jokaisessa toteutuksessa; koska voimme toteuttaa myös Iterator<String> for Counter tai minkä tahansa muun tyypin, Counter-tyypille voi olla useita Iterator-toteutuksia. Toisin sanoen, kun traitilla on geneerinen parametri, sen voi toteuttaa tyypille useita kertoja vaihtamalla geneerisen tyyppiparametrin konkreettista tyyppiä joka kerta. Kun käytämme next-metodia Counter-tyypillä, meidän täytyisi antaa tyyppimerkinnät kertomaan, mitä Iterator-toteutusta haluamme käyttää.

Assosioituneilla tyypeillä emme tarvitse tyyppimerkintöjä, koska emme voi toteuttaa traitia tyypille useita kertoja. Listauksessa 20-13 assosioituneita tyyppejä käyttävällä määritelmällä voimme valita Item-tyypin vain kerran, koska voi olla vain yksi impl Iterator for Counter. Emme joudu määrittämään, että haluamme u32-arvojen iteraattorin, joka kerta kun kutsumme next-metodia Counter-tyypillä.

Assosioituneet tyypit ovat myös osa traitin sopimusta: traitin toteuttajien täytyy tarjota tyyppi assosioituneen tyypin paikkamerkin tilalle. Assosioituilla tyypeillä on usein nimi, joka kuvaa tyypin käyttöä, ja assosioituneen tyypin dokumentointi API-dokumentaatiossa on hyvä käytäntö.

Oletusarvoiset geneeriset parametrit ja operaattorin ylikuormitus

Kun käytämme geneerisiä tyyppiparametreja, voimme määrittää geneeriselle tyypille oletusarvoisen konkreettisen tyypin. Tämä poistaa tarpeen, että traitin toteuttajien täytyy määrittää konkreettinen tyyppi, jos oletustyyppi toimii. Oletustyypin määrittelet geneeristä tyyppiä julistaessasi syntaksilla <PlaceholderType=ConcreteType>.

Hyvä esimerkki tilanteesta, jossa tämä tekniikka on hyödyllinen, on operaattorin ylikuormitus, jossa mukautat operaattorin (kuten +) käyttäytymistä tietyissä tilanteissa.

Rust ei salli omien operaattorien luomista tai mielivaltaisten operaattorien ylikuormitusta. Voit kuitenkin ylikuormittaa std::ops-moduulissa listattuja operaatioita ja niihin liittyviä traitteja toteuttamalla operaattoriin liittyvät traitit. Esimerkiksi listauksessa 20-15 ylikuormitamme +-operaattorin kahden Point-instanssin yhteenlaskuun toteuttamalla Add-traitin Point-rakenteelle.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-15/src/main.rs}}
}
Listing 20-15: Add-traitin toteutus +-operaattorin ylikuormittamiseksi Point-instansseille

add-metodi laskee kahden Point-instanssin x-arvot ja kahden Point-instanssin y-arvot yhteen luodakseen uuden Point-instanssin. Add-traitilla on assosioitu tyyppi nimeltä Output, joka määrittää add-metodin palauttaman tyypin.

Tässä koodissa oletusarvoinen geneerinen tyyppi on Add-traitin sisällä. Tässä on sen määritelmä:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Tämän koodin pitäisi näyttää yleisesti tutulta: trait yhdellä metodilla ja assosioidulla tyypillä. Uutta on Rhs=Self: tätä syntaksia kutsutaan oletusarvoisiksi tyyppiparametreiksi. Geneerinen tyyppiparametri Rhs (lyhenne ”right-hand side”, oikea puoli) määrittää add-metodin rhs-parametrin tyypin. Jos emme määritä konkreettista tyyppiä Rhs-parametrille toteuttaessamme Add-traitia, Rhs-tyypiksi tulee oletuksena Self, eli tyyppi, jolle toteutamme Add-traitin.

Kun toteutimme Add-traitin Point-tyypille, käytimme oletusta Rhs-parametrille, koska halusimme laskea yhteen kaksi Point-instanssia. Katsotaan esimerkkiä, jossa haluamme mukauttaa Rhs-tyypin oletuksen sijaan.

Meillä on kaksi rakennetta, Millimeters ja Meters, jotka tallentavat arvoja eri yksiköissä. Tämä olemassa olevan tyypin ohut kääriminen toiseen rakenteeseen tunnetaan newtype-kuviona, jota käsittelemme tarkemmin ”Ulkoisten traitien toteuttaminen newtype-kuviolla” -kohdassa. Haluamme laskea yhteen millimetreissä olevia arvoja metreissä olevien arvojen kanssa ja Add-toteutuksen tekemän muunnoksen oikein. Voimme toteuttaa Add-traitin Millimeters-tyypille asettaen Rhs-tyypiksi Meters, kuten listauksessa 20-16.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-16/src/lib.rs}}
Listing 20-16: Add-traitin toteutus Millimeters-tyypille Millimeters- ja Meters-arvojen yhteenlaskuun

Lisätäksemme Millimeters- ja Meters-arvoja määrittelemme impl Add<Meters> asettaaksemme Rhs-tyyppiparametrin arvon oletuksen Self sijaan.

Käytät oletusarvoisia tyyppiparametreja pääasiassa kahdella tavalla:

  1. Laajentaaksesi tyyppiä rikkomatta olemassa olevaa koodia
  2. Salliaksesi mukautuksen tapauksissa, joita useimmat käyttäjät eivät tarvitse

Standardikirjaston Add-trait on esimerkki toisesta tarkoituksesta: yleensä lasket yhteen samanlaisia tyyppejä, mutta Add-trait tarjoaa mahdollisuuden mukauttaa tätä. Oletusarvoisen tyyppiparametrin käyttö Add-traitin määritelmässä tarkoittaa, että ylimääräistä parametria ei tarvitse määrittää useimmiten. Toisin sanoen, toteutusboilerplatea ei tarvita, mikä helpottaa traitin käyttöä.

Ensimmäinen tarkoitus on samanlainen kuin toinen, mutta päinvastoin: jos haluat lisätä tyyppiparametrin olemassa olevaan traitiin, voit antaa sille oletusarvon laajentaaksesi traitin toiminnallisuutta rikkomatta olemassa olevaa toteutuskoodia.

Samannimisten metodien erottelu

Mikään Rustissa ei estä traitia sisältämästä metodia, jolla on sama nimi kuin toisen traitin metodilla, eikä Rust estä molempien traitien toteuttamista samalle tyypille. On myös mahdollista toteuttaa metodi suoraan tyypille samalla nimellä kuin trait-metodeilla.

Kun kutsut samannimisiä metodeja, sinun täytyy kertoa Rustille, kumpaa haluat käyttää. Harkitse listauksen 20-17 koodia, jossa olemme määritelleet kaksi traitia, Pilot ja Wizard, joilla molemmilla on metodi nimeltä fly. Toteutamme molemmat traitit tyypille Human, jolla on jo suoraan toteutettu metodi nimeltä fly. Jokainen fly-metodi tekee jotain eri.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-17/src/main.rs:here}}
}
Listing 20-17: Kaksi traitia, joilla on fly-metodi, toteutetaan Human-tyypille, ja fly-metodi toteutetaan suoraan Human-tyypille.

Kun kutsumme fly-metodia Human-instanssilla, kääntäjä oletuksena kutsuu tyypille suoraan toteutettua metodia, kuten listauksessa 20-18.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-18/src/main.rs:here}}
}
Listing 20-18: fly-metodin kutsuminen Human-instanssilla

Tämän koodin ajaminen tulostaa *waving arms furiously*, mikä osoittaa, että Rust kutsui Human-tyypille suoraan toteutettua fly-metodia.

Kutsuaksemme fly-metodeja joko Pilot- tai Wizard-traitista, tarvitsemme eksplisittisemmän syntaksin määrittääksemme, mitä fly-metodia tarkoitamme. Listaus 20-19 demonstroi tätä syntaksia.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-19/src/main.rs:here}}
}
Listing 20-19: Sen määrittäminen, kumman traitin fly-metodia haluamme kutsua

Trait-nimen kirjoittaminen ennen metodin nimeä selventää Rustille, mitä fly-toteutusta haluamme kutsua. Voisimme myös kirjoittaa Human::fly(&person), mikä on vastaava kuin listauksessa 20-19 käyttämämme person.fly(), mutta tämä on hieman pidempi kirjoittaa, jos erottelua ei tarvita.

Tämän koodin ajaminen tulostaa seuraavan:

{{#include ../listings/ch20-advanced-features/listing-20-19/output.txt}}

Koska fly-metodilla on self-parametri, jos meillä olisi kaksi tyyppiä, jotka molemmat toteuttavat yhden traitin, Rust voisi päätellä, mitä trait-toteutusta käyttää self-parametrin tyypin perusteella.

Assosioiduilla funktioilla, jotka eivät ole metodeja, ei kuitenkaan ole self-parametria. Kun useilla tyypeillä tai traitilla on samannimisiä ei-metodifunktioita, Rust ei aina tiedä, mitä tyyppiä tarkoitat, ellet käytä täysin pätevää syntaksia. Esimerkiksi listauksessa 20-20 luomme traitin eläintarhaa varten, joka haluaa nimetä kaikki pentukoirat Spotiksi. Teemme Animal-traitin assosioidulla ei-metodifunktiolla baby_name. Animal-trait toteutetaan rakenteelle Dog, jolle tarjoamme myös suoraan assosioituneen ei-metodifunktion baby_name.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-20/src/main.rs}}
}
Listing 20-20: Trait assosioituneella funktiolla ja tyyppi samannimisellä assosioituneella funktiolla, joka toteuttaa myös traitin

Toteutamme koodin, joka nimeää kaikki pennut Spotiksi, Dog-tyypille määritellyssä baby_name-assosioituneessa funktiossa. Dog-tyyppi toteuttaa myös Animal-traitin, joka kuvaa kaikkien eläinten ominaisuuksia. Koiranpennuja kutsutaan puppies-nimellä, ja tämä ilmaistaan Animal-traitin Dog-toteutuksessa Animal-traitiin liittyvässä baby_name-funktiossa.

main-funktiossa kutsumme Dog::baby_name-funktiota, joka kutsuu suoraan Dog-tyypille määriteltyä assosioitunutta funktiota. Tämä koodi tulostaa seuraavan:

{{#include ../listings/ch20-advanced-features/listing-20-20/output.txt}}

Tämä tuloste ei ole haluamamme. Haluamme kutsua baby_name-funktiota, joka on osa Animal-traitia, jonka toteutimme Dog-tyypille, jotta koodi tulostaisi A baby dog is called a puppy. Listauksessa 20-19 käyttämämme trait-nimen määrittämisen tekniikka ei auta tässä; jos muutamme main-funktion listauksen 20-21 koodiksi, saamme käännösvirheen.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-21/src/main.rs:here}}
Listing 20-21: Yritys kutsua baby_name-funktiota Animal-traitista, mutta Rust ei tiedä mitä toteutusta käyttää

Koska Animal::baby_name-funktiolla ei ole self-parametria ja muitakin tyyppejä voi toteuttaa Animal-traitin, Rust ei voi päätellä, mitä Animal::baby_name-toteutusta haluamme. Saamme tämän kääntäjävirheen:

{{#include ../listings/ch20-advanced-features/listing-20-21/output.txt}}

Erottellaksemme ja kertoaksemme Rustille, että haluamme käyttää Animal-traitin Dog-toteutusta toisen tyypin Animal-toteutuksen sijaan, meidän täytyy käyttää täysin pätevää syntaksia. Listaus 20-22 näyttää, miten täysin pätevää syntaksia käytetään.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-22/src/main.rs:here}}
}
Listing 20-22: Täysin pätevän syntaksin käyttö määrittämään, että haluamme kutsua Animal-traitin baby_name-funktiota Dog-toteutuksena

Annamme Rustille tyyppimerkinnän kulmasulkeissa, joka osoittaa, että haluamme kutsua Animal-traitin baby_name-metodia Dog-toteutuksena käsittelemällä Dog-tyyppiä Animal-tyyppinä tässä funktiokutsussa. Tämä koodi tulostaa nyt haluamamme:

{{#include ../listings/ch20-advanced-features/listing-20-22/output.txt}}

Yleisesti täysin pätevä syntaksi määritellään seuraavasti:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Assosioiduille funktioille, jotka eivät ole metodeja, ei olisi vastaanottajaa: olisi vain muiden argumenttien lista. Voit käyttää täysin pätevää syntaksia kaikkialla, missä kutsut funktioita tai metodeja. Voit kuitenkin jättää pois kaiken osan tästä syntaksista, jonka Rust voi päätellä muusta ohjelman tiedosta. Tarvitset tämän monimutkaisemman syntaksin vain tapauksissa, joissa on useita samannimisiä toteutuksia ja Rust tarvitsee apua tunnistamaan, mitä toteutusta haluat kutsua.

Supertraitien käyttö

Joskus saatat kirjoittaa trait-määritelmän, joka riippuu toisesta traitista: jotta tyyppi toteuttaisi ensimmäisen traitin, haluat vaatia sen toteuttavan myös toisen traitin. Teet näin, jotta trait-määritelmäsi voi käyttää toisen traitin assosioituja kohteita. Trait, johon trait-määritelmäsi nojaa, on traitisi supertrait.

Esimerkiksi haluamme tehdä OutlinePrint-traitin, jolla on outline_print-metodi, joka tulostaa annetun arvon muotoiltuna niin, että se on kehystetty tähtimerkeillä. Eli kun Point-rakenteella, joka toteuttaa standardikirjaston Display-traitin tulostaakseen (x, y), kutsumme outline_print-metodia Point-instanssilla, jossa x on 1 ja y on 3, sen pitäisi tulostaa seuraava:

**********
*        *
* (1, 3) *
*        *
**********

outline_print-metodin toteutuksessa haluamme käyttää Display-traitin toiminnallisuutta. Siksi meidän täytyy määrittää, että OutlinePrint-trait toimii vain tyypeille, jotka myös toteuttavat Display-traitin ja tarjoavat OutlinePrint-traitin tarvitseman toiminnallisuuden. Voimme tehdä sen trait-määritelmässä määrittämällä OutlinePrint: Display. Tämä tekniikka muistuttaa trait-sidontojen lisäämistä traitiin. Listaus 20-23 näyttää OutlinePrint-traitin toteutuksen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-23/src/main.rs:here}}
}
Listing 20-23: OutlinePrint-traitin toteutus, joka vaatii toiminnallisuutta Display-traitista

Koska olemme määrittäneet, että OutlinePrint vaatii Display-traitin, voimme käyttää to_string-funktiota, joka toteutetaan automaattisesti kaikille Display-traitin toteuttaville tyypeille. Jos yrittäisimme käyttää to_string-funktiota lisäämättä kaksoispistettä ja Display-traitia trait-nimen jälkeen, saisimme virheen, jossa sanotaan, ettei tyypille &Self löydy metodia nimeltä to_string nykyisestä näkyvyysalueesta.

Katsotaan, mitä tapahtuu, kun yritämme toteuttaa OutlinePrint-traitin tyypille, joka ei toteuta Display-traitia, kuten Point-rakenteelle:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-02-impl-outlineprint-for-point/src/main.rs:here}}

Saamme virheen, jossa sanotaan, että Display vaaditaan mutta sitä ei ole toteutettu:

{{#include ../listings/ch20-advanced-features/no-listing-02-impl-outlineprint-for-point/output.txt}}

Korjataksemme tämän toteutamme Display-traitin Point-tyypille ja täytämme OutlinePrint-traitin vaatiman rajoitteen näin:

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-03-impl-display-for-point/src/main.rs:here}}
}

Sitten OutlinePrint-traitin toteuttaminen Point-tyypille kääntyy onnistuneesti, ja voimme kutsua outline_print-metodia Point-instanssilla näyttääksemme sen tähtimerkkikehyksessä.

Ulkoisten traitien toteuttaminen newtype-kuviolla

Luvun 10 ”Traitin toteuttaminen tyypille” -kohdassa mainitsimme orporoolin, jonka mukaan saamme toteuttaa traitin tyypille vain, jos trait tai tyyppi, tai molemmat, ovat paikallisia kratellemme. Tämän rajoituksen voi kiertää newtype-kuviolla, jossa luodaan uusi tyyppi monikkorakenteessa. (Käsittelimme monikkorakenteita luvun 5 ”Eri tyyppien luominen monikkorakenteilla” -kohdassa.) Monikkorakenteella on yksi kenttä, ja se on ohut kääre tyypin ympärillä, jolle haluamme toteuttaa traitin. Kääretyyppi on sitten paikallinen kratellemme, ja voimme toteuttaa traitin kääretyypille. Newtype on termi, joka on peräisin Haskell-ohjelmointikielestä. Tämän kuvion käytöstä ei ole ajonaikaisen suorituskyvyn rangaistusta, ja kääretyyppi häviää käännösaikana.

Esimerkkinä haluamme toteuttaa Display-traitin tyypille Vec<T>, mitä orporooli estää tekemästä suoraan, koska Display-trait ja Vec<T>-tyyppi on määritelty kratellemme ulkopuolella. Voimme tehdä Wrapper-rakenteen, joka pitää Vec<T>-instanssin; sitten voimme toteuttaa Display-traitin Wrapper-tyypille ja käyttää Vec<T>-arvoa, kuten listauksessa 20-24.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-24/src/main.rs}}
}
Listing 20-24: Wrapper-tyypin luominen Vec<String>-tyypin ympärille Display-traitin toteuttamiseksi

Display-toteutus käyttää self.0 päästäkseen sisäiseen Vec<T>-arvoon, koska Wrapper on monikkorakenne ja Vec<T> on monikon indeksin 0 kohde. Sitten voimme käyttää Display-traitin toiminnallisuutta Wrapper-tyypillä.

Tämän tekniikan haittapuoli on, että Wrapper on uusi tyyppi, joten sillä ei ole sen pitämän arvon metodeja. Meidän täytyisi toteuttaa kaikki Vec<T>-metodit suoraan Wrapper-tyypille niin, että metodit delegoivat self.0:lle, jotta voisimme käsitellä Wrapper-tyyppiä täsmälleen kuten Vec<T>-tyyppiä. Jos haluaisimme uudella tyypillä kaikki sisäisen tyypin metodit, ratkaisu olisi toteuttaa Deref-trait Wrapper-tyypille palauttaakseen sisäisen tyypin (käsittelimme Deref-traitin toteutusta luvun 15 ”Älyosoittimien käyttely tavallisten viitteiden tavoin” -kohdassa). Jos emme haluaisi Wrapper-tyypillä kaikkia sisäisen tyypin metodeja — esimerkiksi rajoittaaksemme Wrapper-tyypin käyttäytymistä — meidän täytyisi toteuttaa manuaalisesti vain haluamamme metodit.

Tämä newtype-kuvio on hyödyllinen myös silloin, kun traitteja ei ole mukana. Siirrytään seuraavaksi tarkastelemaan edistyneitä tapoja vuorovaikuttaa Rustin tyyppijärjestelmän kanssa.

Kehittyneet tyypit

Edistyneet tyypit

Rustin tyyppijärjestelmässä on ominaisuuksia, joista olemme maininneet mutta joita emme ole vielä käsitelleet. Aloitamme käsittelemällä newtype-tyyppejä yleisesti ja tutkimalla, miksi ne ovat hyödyllisiä tyyppeinä. Siirrymme sitten tyyppialiasiin, ominaisuuteen, joka muistuttaa newtype-tyyppejä mutta hieman eri semantiikalla. Käsittelemme myös !-tyypin ja dynaamisesti mitoitetut tyypit.

Tyyppiturvallisuus ja abstraktio newtype-kuviolla

Tämä osio olettaa, että olet lukenut aiemman ”Ulkoisten traitien toteuttaminen newtype-kuviolla” -kohdan. Newtype-kuvio on hyödyllinen myös muissa tehtävissä kuin niissä, joita olemme tähän asti käsitelleet, mukaan lukien arvojen sekoittumisen estäminen staattisesti ja arvon yksiköiden ilmaiseminen. Näit esimerkin newtype-tyyppien käytöstä yksiköiden ilmaisemiseen listauksessa 20-16: muista, että Millimeters- ja Meters-rakenteet käärivät u32-arvot newtype-tyyppiin. Jos kirjoittaisimme funktion, jonka parametri on tyyppiä Millimeters, emme voisi kääntää ohjelmaa, joka yrittäisi vahingossa kutsua funktiota Meters- tai tavallisella u32-arvolla.

Voimme käyttää newtype-kuviota myös piilottaaksemme tyypin toteutuksen yksityiskohtia: uusi tyyppi voi tarjota julkisen API:n, joka eroaa sisäisen yksityisen tyypin API:sta.

Newtype-tyypit voivat myös piilottaa sisäisen toteutuksen. Esimerkiksi voisimme tarjota People-tyypin käärimään HashMap<i32, String>-rakenteen, joka tallentaa henkilön tunnuksen ja nimen. People-tyyppiä käyttävä koodi käyttäisi vain tarjoamaamme julkista API:a, kuten metodia nimen lisäämiseen People-kokoelmaan; koodin ei tarvitse tietää, että liitämme nimiin sisäisesti i32-tunnuksen. Newtype-kuvio on kevyt tapa saavuttaa kapselointi toteutuksen yksityiskohtien piilottamiseksi, josta puhuimme luvun 18 ”Toteutuksen yksityiskohdat piilottava kapselointi” -kohdassa.

Tyyppisynonyymit ja tyyppialiasit

Rust tarjoaa mahdollisuuden julistaa tyyppialiasin antaakseen olemassa olevalle tyypille toisen nimen. Tätä varten käytämme type-avainsanaa. Esimerkiksi voimme luoda aliasin Kilometers tyypille i32 näin:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs:here}}
}

Nyt alias Kilometers on synonyymi tyypille i32; toisin kuin listauksessa 20-16 luodut Millimeters- ja Meters-tyypit, Kilometers ei ole erillinen uusi tyyppi. Kilometers-tyypin arvoja käsitellään samoin kuin i32-tyypin arvoja:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs:there}}
}

Koska Kilometers ja i32 ovat sama tyyppi, voimme laskea yhteen molempien tyyppien arvoja ja välittää Kilometers-arvoja funktioille, jotka ottavat i32-parametreja. Tällä tavalla emme kuitenkaan saa tyyppitarkistuksen etuja, joita saamme aiemmin käsitellystä newtype-kuviosta. Toisin sanoen, jos sekoitamme Kilometers- ja i32-arvoja jossain, kääntäjä ei anna virhettä.

Tyyppisynonyymien pääasiallinen käyttötarkoitus on toiston vähentäminen. Esimerkiksi meillä voi olla pitkä tyyppi kuten tämä:

Box<dyn Fn() + Send + 'static>

Tämän pitkän tyypin kirjoittaminen funktiosignatuureissa ja tyyppimerkinnöissä koko koodissa voi olla työlästä ja virhealtista. Kuvittele projekti, joka on täynnä tällaista koodia kuten listauksessa 20-25.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-25/src/main.rs:here}}
}
Listing 20-25: Pitkän tyypin käyttö monessa paikassa

Tyyppialias tekee tästä koodista hallittavampaa vähentämällä toistoa. Listauksessa 20-26 olemme tuoneet aliasin nimeltä Thunk monimutkaiselle tyypille ja voimme korvata kaikki tyypin käytöt lyhyemmällä aliasilla Thunk.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-26/src/main.rs:here}}
}
Listing 20-26: Tyyppialiasin Thunk käyttöönotto toiston vähentämiseksi

Tätä koodia on paljon helpompi lukea ja kirjoittaa! Tyyppialiasille merkityksellisen nimen valinta auttaa myös viestimään aikomuksestasi (thunk tarkoittaa koodia, joka arvioidaan myöhemmin, joten se on sopiva nimi tallennettavalle sulkeumalle).

Tyyppialiasit ovat myös yleisiä Result<T, E>-tyypin kanssa toiston vähentämiseksi. Harkitse standardikirjaston std::io-moduulia. I/O-operaatiot palauttavat usein Result<T, E>-tyypin käsitelläkseen tilanteita, joissa operaatiot eivät onnistu. Tässä kirjastossa on std::io::Error-rakenne, joka edustaa kaikkia mahdollisia I/O-virheitä. Monet std::io-moduulin funktiot palauttavat Result<T, E>-tyypin, jossa E on std::io::Error, kuten nämä Write-traitin funktiot:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-05-write-trait/src/lib.rs}}

Result<..., Error> toistuu paljon. Siksi std::io-moduulissa on tämä tyyppialiasin:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rs:here}}

Koska tämä julistus on std::io-moduulissa, voimme käyttää täysin pätevää aliasia std::io::Result<T>; eli Result<T, E>-tyyppiä, jossa E on std::io::Error. Write-traitin funktiosignatuurit näyttävät lopulta tältä:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rs:there}}

Tyyppialias auttaa kahdella tavalla: se helpottaa koodin kirjoittamista ja tarjoaa yhtenäisen rajapinnan koko std::io-moduulissa. Koska se on alias, se on vain toinen Result<T, E>, mikä tarkoittaa, että voimme käyttää kaikkia Result<T, E>-tyypin metodeja sen kanssa sekä erityissyntaksia kuten ?-operaattoria.

Never-tyyppi, joka ei koskaan palaa

Rustissa on erityinen tyyppi nimeltä !, jota työteorian kielessä kutsutaan tyhjäksi tyypiksi, koska sillä ei ole arvoja. Kutsumme sitä mieluummin never-tyypiksi, koska se edustaa palautustyyppiä, kun funktio ei koskaan palaa. Tässä on esimerkki:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-07-never-type/src/lib.rs:here}}

Tämä koodi luetaan: ”funktio bar palauttaa never-tyypin.” Funktioita, jotka eivät koskaan palaavat, kutsutaan hajautuviksi funktioiksi. Emme voi luoda !-tyypin arvoja, joten bar ei voi koskaan palata.

Mutta mitä hyötyä on tyypistä, jonka arvoja ei voi koskaan luoda? Muista listauksen 2-5 koodi numeronarvauspelistä; olemme toistaneet osan siitä listauksessa 20-27.

{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:ch19}}
Listing 20-27: match-lauseke, jonka haara päättyy continue-lauseeseen

Silloin ohitimme joitakin yksityiskohtia tässä koodissa. Luvun 6 match-ohjausrakenne” -kohdassa käsittelimme, että match-haarojen täytyy kaikkien palauttaa sama tyyppi. Esimerkiksi seuraava koodi ei toimi:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-08-match-arms-different-types/src/main.rs:here}}

Tässä koodissa guess-muuttujan tyypin pitäisi olla sekä kokonaisluku että merkkijono, ja Rust vaatii, että guess-muuttujalla on vain yksi tyyppi. Mitä siis continue palauttaa? Miten saimme palauttaa u32-arvon yhdestä haarasta ja toisessa haarassa continue-lauseen listauksessa 20-27?

Kuten saatoit arvata, continue-lauseella on !-arvon tyyppi. Eli kun Rust laskee guess-muuttujan tyypin, se katsoo molemmat match-haarat: edellisessä arvo on u32 ja jälkimmäisessä !-arvon tyyppi. Koska !-tyypillä ei voi koskaan olla arvoa, Rust päättelee, että guess-muuttujan tyyppi on u32.

Tätä käyttäytymistä kuvataan muodollisesti niin, että !-tyypin lausekkeet voidaan pakottaa mihin tahansa muuhun tyyppiin. Saamme päättää tämän match-haaran continue-lauseella, koska continue ei palauta arvoa; sen sijaan se siirtää ohjauksen silmukan alkuun, joten Err-tapauksessa emme koskaan sijoita arvoa guess-muuttujaan.

Never-tyyppi on hyödyllinen myös panic!-makron kanssa. Muista unwrap-funktio, jota kutsumme Option<T>-arvoilla saadaksemme arvon tai panikoidaksemme tämän määritelmän mukaisesti:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-09-unwrap-definition/src/lib.rs:here}}

Tässä koodissa tapahtuu sama kuin listauksen 20-27 match-lausekkeessa: Rust näkee, että val on tyyppiä T ja panic! on tyyppiä !, joten koko match-lausekkeen tulos on T. Tämä koodi toimii, koska panic! ei tuota arvoa; se lopettaa ohjelman. None-tapauksessa emme palauta arvoa unwrap-funktiosta, joten tämä koodi on kelvollinen.

Viimeinen lauseke, jolla on tyyppi !, on silmukka:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-10-loop-returns-never/src/main.rs:here}}

Tässä silmukka ei koskaan pääty, joten lausekkeen arvo on !. Tämä ei kuitenkaan pitäisi paikkaansa, jos sisällyttäisimme break-lauseen, koska silmukka päättyisi break-lauseeseen.

Dynaamisesti mitoitetut tyypit ja Sized-trait

Rustin täytyy tietää tiettyjä yksityiskohtia tyypeistään, kuten kuinka paljon tilaa varata tietyn tyypin arvolle. Tämä jättää tyyppijärjestelmän yhden nurkan aluksi hieman hämmentäväksi: dynaamisesti mitoitettujen tyyppien käsite. Joskus kutsutaan DST-tyypeiksi tai kokoon mitoittamattomiksi tyypeiksi, nämä tyypit sallivat koodin kirjoittamisen arvoilla, joiden koon tiedämme vasta ajonaikana.

Syvennytään dynaamisesti mitoitetun tyypin str yksityiskohtiin, jota olemme käyttäneet koko kirjan ajan. Aivan oikein, ei &str vaan pelkkä str on DST. Monissa tapauksissa, kuten kun tallennamme käyttäjän syöttämää tekstiä, emme voi tietää merkkijonon pituutta ennen ajonaikaa. Tämä tarkoittaa, ettemme voi luoda str-tyyppistä muuttujaa emmekä ottaa str-tyyppistä argumenttia. Harkitse seuraavaa koodia, joka ei toimi:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-11-cant-create-str/src/main.rs:here}}

Rustin täytyy tietää, kuinka paljon muistia varata minkä tahansa tietyn tyypin arvolle, ja kaikkien saman tyypin arvojen täytyy käyttää saman verran muistia. Jos Rust sallisi tämän koodin kirjoittamisen, nämä kaksi str-arvoa tarvitsisivat saman verran tilaa. Niillä on kuitenkin eri pituudet: s1 tarvitsee 12 tavua tallennustilaa ja s2 tarvitsee 15. Siksi dynaamisesti mitoitetun tyypin muuttujaa ei voi luoda.

Mitä siis teemme? Tässä tapauksessa tiedät jo vastauksen: teemme s1:n ja s2:n tyypiksi merkkijonoviipaleen (&str) eikä str. Muista luvun 4 ”Merkkijonoviipaleet” -kohdasta, että viipaleen tietorakenne tallentaa vain aloitusposition ja viipaleen pituuden. Vaikka &T on yksi arvo, joka tallentaa muistiosoitteen, jossa T sijaitsee, merkkijonoviipale on kaksi arvoa: str:n osoite ja sen pituus. Näin ollen merkkijonoviipaleen arvon koon tiedämme käännösaikana: se on kaksi kertaa usize:n pituus. Eli tiedämme aina merkkijonoviipaleen koon riippumatta siitä, kuinka pitkä viipaleen viittaama merkkijono on. Yleisesti tämä on tapa, jolla dynaamisesti mitoitettuja tyyppejä käytetään Rustissa: niillä on ylimääräinen metatieto, joka tallentaa dynaamisen tiedon koon. Dynaamisesti mitoitettujen tyyppien kultainen sääntö on, että dynaamisesti mitoitettujen tyyppien arvot täytyy aina sijoittaa jonkinlaisen osoittimen taakse.

Voimme yhdistää str:n kaikenlaisiin osoittimiin: esimerkiksi Box<str> tai Rc<str>. Itse asiassa olet nähnyt tämän aiemmin, mutta eri dynaamisesti mitoitetulla tyypillä: traitit. Jokainen trait on dynaamisesti mitoitettu tyyppi, johon voimme viitata traitin nimellä. Luvun 18 ”Trait-olioiden käyttö yhteisen käyttäytymisen abstrahoimiseen” -kohdassa mainitsimme, että traitien käyttämiseksi trait-olioina meidän täytyy sijoittaa ne osoittimen taakse, kuten &dyn Trait tai Box<dyn Trait> (Rc<dyn Trait> toimisi myös).

DST-tyyppien kanssa työskentelyyn Rust tarjoaa Sized-traitin määrittämään, tiedetäänkö tyypin koko käännösaikana. Tämä trait toteutetaan automaattisesti kaikelle, jonka koko tiedetään käännösaikana. Lisäksi Rust lisää implisiittisesti Sized-sidonnan jokaiseen geneeriseen funktioon. Eli geneerinen funktiomääritelmä kuten tämä:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-12-generic-fn-definition/src/lib.rs}}

käsitellään ikään kuin olisimme kirjoittaneet tämän:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-13-generic-implicit-sized-bound/src/lib.rs}}

Oletuksena geneeriset funktiot toimivat vain tyypeillä, joiden koko tunnetaan käännösaikana. Voit kuitenkin käyttää seuraavaa erityissyntaksia tämän rajoituksen lieventämiseksi:

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-14-generic-maybe-sized/src/lib.rs}}

?Sized-trait-sidonta tarkoittaa ”T voi olla tai olla olematta Sized”, ja tämä merkintä ohittaa oletuksen, että geneeristen tyyppien täytyy olla koon tiedetty käännösaikana. ?Trait-syntaksi tällä merkityksellä on saatavilla vain Sized-traitille, ei muille traitille.

Huomaa myös, että vaihdoimme t-parametrin tyypin T:stä &T:hen. Koska tyyppi ei välttämättä ole Sized, meidän täytyy käyttää sitä jonkinlaisen osoittimen kautta. Tässä tapauksessa valitsimme viitteen.

Seuraavaksi puhumme funktioista ja sulkeumista!

Kehittyneet funktiot ja sulkeiset

Edistyneet funktiot ja sulkeumat

Tämä osio tutkii edistyneitä ominaisuuksia, jotka liittyvät funktioihin ja sulkeumiin, mukaan lukien funktio-osoittimet ja sulkeumien palauttaminen.

Funktio-osoittimet

Olemme puhuneet sulkeumien välittämisestä funktioille; voit myös välittää tavallisia funktioita funktioille! Tämä tekniikka on hyödyllinen, kun haluat välittää jo määrittelemäsi funktion uuden sulkeuman määrittelemisen sijaan. Funktiot pakotetaan tyypiksi fn (pienellä f:llä), älä sekoita sitä Fn-sulkeumatraitiin. fn-tyyppiä kutsutaan funktio-osoittimeksi. Funktioiden välittäminen funktio-osoittimilla sallii funktioiden käytön argumentteina muille funktioille.

Syntaksi parametrin määrittämiseksi funktio-osoittimeksi on samankaltainen kuin sulkeumilla, kuten listauksessa 20-28, jossa olemme määritelleet funktion add_one, joka lisää 1 parametrilleen. Funktio do_twice ottaa kaksi parametria: funktio-osoittimen mihin tahansa funktioon, joka ottaa i32-parametrin ja palauttaa i32:n, sekä yhden i32-arvon. Funktio do_twice kutsuu funktiota f kahdesti välittäen sille arg-arvon ja laskee sitten kaksi funktiokutsun tulosta yhteen. main-funktio kutsuu do_twice-funktiota argumenteilla add_one ja 5.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-28/src/main.rs}}
}
Listing 20-28: fn-tyypin käyttö funktio-osoittimen hyväksymiseksi argumenttina

Tämä koodi tulostaa The answer is: 12. Määrittelemme, että do_twice-funktion parametri f on fn, joka ottaa yhden i32-tyyppisen parametrin ja palauttaa i32:n. Voimme sitten kutsua f:ää do_twice-funktion rungossa. main-funktiossa voimme välittää funktion nimen add_one ensimmäisenä argumenttina do_twice-funktiolle.

Toisin kuin sulkeumat, fn on tyyppi eikä trait, joten määrittelemme fn:n suoraan parametrityypiksi sen sijaan, että julistaisimme geneerisen tyyppiparametrin yhdellä Fn-traiteista trait-sidonnana.

Funktio-osoittimet toteuttavat kaikki kolme sulkeumatraitia (Fn, FnMut ja FnOnce), mikä tarkoittaa, että voit aina välittää funktio-osoittimen argumenttina funktiolle, joka odottaa sulkeumaa. On parasta kirjoittaa funktiot käyttämällä geneeristä tyyppiä ja yhtä sulkeumatraitia, jotta funktiosi voivat hyväksyä joko funktioita tai sulkeumia.

Siitä huolimatta yksi esimerkki tilanteesta, jossa haluat hyväksyä vain fn:n eikä sulkeumia, on vuorovaikutus ulkoisen koodin kanssa, jolla ei ole sulkeumia: C-funktiot voivat ottaa funktioita argumentteina, mutta C:ssä ei ole sulkeumia.

Esimerkkinä tilanteesta, jossa voisit käyttää joko rivillä määriteltyä sulkeumaa tai nimettyä funktiota, katsotaan standardikirjaston Iterator-traitin tarjoaman map-metodin käyttöä. Käyttääksemme map-metodia muuntaaksemme numerovektorin merkkijonovektoriksi, voisimme käyttää sulkeumaa, kuten listauksessa 20-29.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-29/src/main.rs:here}}
}
Listing 20-29: Sulkeuman käyttö map-metodin kanssa numeroiden muuntamiseksi merkkijonoiksi

Tai voisimme nimetä funktion map-metodin argumentiksi sulkeuman sijaan. Listaus 20-30 näyttää, miltä tämä näyttäisi.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-30/src/main.rs:here}}
}
Listing 20-30: String::to_string-funktion käyttö map-metodin kanssa numeroiden muuntamiseksi merkkijonoiksi

Huomaa, että meidän täytyy käyttää täysin pätevää syntaksia, josta puhuimme ”Edistyneet traitit” -osiossa, koska useita samannimisiä funktioita on saatavilla.

Tässä käytämme ToString-traitissa määriteltyä to_string-funktiota, jonka standardikirjasto on toteuttanut kaikille Display-traitin toteuttaville tyypeille.

Muista luvun 6 ”Luettelotyyppien arvot” -kohdasta, että jokaisesta määrittelemästämme luettelotyypin variantista tulee myös alustusfunktio. Voimme käyttää näitä alustusfunktioita funktio-osoittimina, jotka toteuttavat sulkeumatraitit, mikä tarkoittaa, että voimme määrittää alustusfunktiot argumenteiksi metodeille, jotka ottavat sulkeumia, kuten listauksessa 20-31.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-31/src/main.rs:here}}
}
Listing 20-31: Luettelotyypin alustajan käyttö map-metodin kanssa Status-instanssien luomiseksi numeroista

Tässä luomme Status::Value-instansseja jokaisesta u32-arvosta välillä, jolla map kutsutaan, käyttämällä Status::Value-variantin alustusfunktiota. Jotkut pitävät tästä tyylistä ja jotkut sulkeumista. Ne kääntyvät samaan koodiin, joten käytä kumpaa tahansa tyyliä, joka on sinulle selkeämpi.

Sulkeumien palauttaminen

Sulkeumat edustetaan traitteina, mikä tarkoittaa, että et voi palauttaa sulkeumia suoraan. Useimmissa tapauksissa, joissa haluaisit palauttaa traitin, voit sen sijaan käyttää traitin toteuttavaa konkreettista tyyppiä funktion paluuarvona. Sulkeumien kanssa et kuitenkaan voi yleensä tehdä niin, koska niillä ei ole palautettavaa konkreettista tyyppiä; et saa käyttää funktio-osoitinta fn paluutyyppinä, jos sulkeuma sieppaa arvoja näkyvyysalueestaan.

Sen sijaan käytät normaalisti luvussa 10 opittua impl Trait -syntaksia. Voit palauttaa minkä tahansa funktiotyypin käyttämällä Fn-, FnOnce- ja FnMut-traitteja. Esimerkiksi listauksen 20-32 koodi kääntyy aivan hyvin.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-32/src/lib.rs}}
}
Listing 20-32: Sulkeuman palauttaminen funktiosta impl Trait -syntaksia käyttäen

Kuten totesimme luvun 13 ”Sulkeumatyyppien päättely ja merkitseminen” -kohdassa, jokainen sulkeuma on myös oma erillinen tyypkinsä. Jos sinun täytyy työskennellä useiden saman signatuurin mutta eri toteutuksen funktioiden kanssa, tarvitset niille trait-olion. Harkitse, mitä tapahtuu, jos kirjoitat listauksen 20-33 kaltaista koodia.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-33/src/main.rs}}
Listing 20-33: Vec<T>-vektorin luominen sulkeumista, jotka on määritelty funktioilla, jotka palauttavat impl Fn -tyyppejä

Tässä meillä on kaksi funktiota, returns_closure ja returns_initialized_closure, jotka molemmat palauttavat impl Fn(i32) -> i32. Huomaa, että ne palauttamat sulkeumat ovat erilaisia, vaikka ne toteuttavat saman tyypin. Jos yritämme kääntää tämän, Rust kertoo, ettei se toimi:

{{#include ../listings/ch20-advanced-features/listing-20-33/output.txt}}

Virheilmoitus kertoo, että aina kun palautamme impl Trait -tyypin, Rust luo ainutlaatuisen läpinäkymättömän tyypin, tyypin, jonka yksityiskohtiin emme voi kurkistaa emmekä voi arvata, minkä tyypin Rust luo kirjoittaaksemme sen itse. Vaikka nämä funktiot palauttavat sulkeumia, jotka toteuttavat saman traitin Fn(i32) -> i32, Rustin luomat läpinäkymättömät tyypit ovat erilaisia. (Tämä on samankaltaista kuin se, miten Rust tuottaa erilaisia konkreettisia tyyppejä eri async-lohkoille, vaikka niillä olisi sama tulostyyppi, kuten näimme luvun 17 Pin-tyyppi ja Unpin-trait” -kohdassa.) Olemme nähneet ratkaisun tähän ongelmaan jo useita kertoja: voimme käyttää trait-oliota, kuten listauksessa 20-34.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-34/src/main.rs:here}}
}
Listing 20-34: Vec<T>-vektorin luominen sulkeumista, jotka on määritelty funktioilla, jotka palauttavat Box<dyn Fn> -tyypin, jotta niillä on sama tyyppi

Tämä koodi kääntyy aivan hyvin. Lisätietoa trait-olioista on luvun 18 ”Trait-olioiden käyttö yhteisen käyttäytymisen abstrahoimiseen” -kohdassa.

Seuraavaksi katsotaan makroja!

Makrot

Makrot

Olemme käyttäneet makroja kuten println! koko kirjan ajan, mutta emme ole täysin tutkineet, mikä makro on ja miten se toimii. Termi makro viittaa Rustin ominaisuusperheeseen — deklaratiivisiin makroihin macro_rules!-avainsanalla ja kolmeen proseduraalisen makron lajiin:

  • Mukautetut #[derive]-makrot, jotka määrittävät derive-attribuutilla lisättävän koodin rakenteille ja luettelotyypeille
  • Attribuuttimaiset makrot, jotka määrittelevät mukautettuja attribuutteja käytettäväksi millä tahansa kohteella
  • Funktiomaiset makrot, jotka näyttävät funktiokutsuilta mutta toimivat argumenttina annetuilla tokeneilla

Käsittelemme kukin näistä vuorollaan, mutta ensin katsotaan, miksi tarvitsemme makroja, kun meillä on jo funktiot.

Ero makrojen ja funktioiden välillä

Pohjimmiltaan makrot ovat tapa kirjoittaa koodia, joka kirjoittaa muuta koodia; tätä kutsutaan metaprogrammoinniksi. Liitteessä C käsittelemme derive-attribuuttia, joka luo eri traitien toteutuksia puolestasi. Olemme myös käyttäneet println!- ja vec!-makroja koko kirjan ajan. Kaikki nämä makrot laajenevat tuottaakseen enemmän koodia kuin mitä olet kirjoittanut manuaalisesti.

Metaprogrammointi on hyödyllistä vähentämään kirjoitettavan ja ylläpidettävän koodin määrää, mikä on myös yksi funktioiden rooleista. Makroilla on kuitenkin lisävoimia, joita funktioilla ei ole.

Funktiosignatuurissa täytyy julistaa funktion parametrien määrä ja tyypit. Makrot sen sijaan voivat ottaa muuttuvan määrän parametreja: voimme kutsua println!("hello") yhdellä argumentilla tai println!("hello {}", name) kahdella argumentilla. Lisäksi makrot laajennetaan ennen kuin kääntäjä tulkitsee koodin merkityksen, joten makro voi esimerkiksi toteuttaa traitin tietylle tyypille. Funktio ei voi, koska sitä kutsutaan ajonaikana ja trait täytyy toteuttaa käännösaikana.

Makron toteuttamisen haittapuoli funktion sijaan on, että makromääritelmät ovat monimutkaisempia kuin funktiomääritelmät, koska kirjoitat Rust-koodia, joka kirjoittaa Rust-koodia. Tämän epäsuoran viittauksen vuoksi makromääritelmiä on yleensä vaikeampi lukea, ymmärtää ja ylläpitää kuin funktiomääritelmiä.

Toinen tärkeä ero makrojen ja funktioiden välillä on, että makrot täytyy määritellä tai tuoda näkyvyysalueelle ennen kuin kutsut niitä tiedostossa, toisin kuin funktiot, jotka voi määritellä missä tahansa ja kutsua missä tahansa.

Deklaratiiviset makrot yleiseen metaprogrammointiin

Yleisin makrojen muoto Rustissa on deklaratiivinen makro. Niitä kutsutaan joskus myös ”makroiksi esimerkin mukaan”, ”macro_rules!-makroiksi” tai yksinkertaisesti ”makroiksi”. Ytimessään deklaratiiviset makrot sallivat kirjoittaa jotain, mikä muistuttaa Rustin match-lauseketta. Kuten luvussa 6 käsiteltiin, match-lausekkeet ovat ohjausrakenteita, jotka ottavat lausekkeen, vertaavat lausekkeen tulosarvoa kuvioihin ja suorittavat sitten kuvioon liittyvän koodin. Makrot vertaavat myös arvoa kuvioihin, joihin liittyy tietty koodi: tässä tilanteessa arvo on makrolle välitetty kirjaimellinen Rust-lähdekoodi; kuviot verrataan kyseisen lähdekoodin rakenteeseen; ja kuhunkin kuvioon liittyvä koodi korvaa makrolle välitetyn koodin, kun kuvio täsmää. Kaikki tämä tapahtuu käännöksen aikana.

Makron määrittelyssä käytetään macro_rules!-rakennetta. Tutkitaan macro_rules!-käyttöä katsomalla, miten vec!-makro on määritelty. Luvussa 8 käsittelimme vec!-makron käyttöä uuden vektorin luomiseen tietyillä arvoilla. Esimerkiksi seuraava makro luo uuden vektorin, joka sisältää kolme kokonaislukua:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

Voisimme käyttää vec!-makroa myös kahden kokonaisluvun vektorin tai viiden merkkijonoviipaleen vektorin luomiseen. Emme voisi tehdä samaa funktiolla, koska emme tietäisi etukäteen arvojen määrää tai tyyppiä.

Listaus 20-35 näyttää hieman yksinkertaistetun vec!-makron määritelmän.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-35/src/lib.rs}}
Listing 20-35: Yksinkertaistettu vec!-makron määritelmä

Huom: Standardikirjaston todellinen vec!-makron määritelmä sisältää koodia oikean muistimäärän varaamiseksi etukäteen. Tämä koodi on optimointi, jota emme sisällytä tähän esimerkin yksinkertaistamiseksi.

#[macro_export]-annotaatio osoittaa, että tämä makro tulisi olla käytettävissä aina, kun makron määrittelevä krate tuodaan näkyvyysalueelle. Ilman tätä annotaatiota makroa ei voi tuoda näkyvyysalueelle.

Aloitamme makromääritelmän macro_rules!-avainsanalla ja määrittelemämme makron nimellä ilman huutomerkkiä. Nimeä, tässä tapauksessa vec, seuraa aaltosulkeet, jotka merkitsevät makromääritelmän runkoa.

vec!-rungon rakenne muistuttaa match-lausekkeen rakennetta. Tässä on yksi haara kuviolla ( $( $x:expr ),* ), jota seuraa => ja tähän kuvioon liittyvä koodilohko. Jos kuvio täsmää, siihen liittyvä koodilohko emitoidaan. Koska tämä on ainoa kuvio tässä makrossa, on vain yksi kelvollinen täsmäystapa; mikä tahansa muu kuvio johtaa virheeseen. Monimutkaisemmilla makroilla on useampia haaroja.

Kelvollinen kuviosyntaksi makromääritelmissä eroaa luvussa 19 käsitellystä kuviosyntaksista, koska makrokuviot täsmätään Rust-koodin rakenteeseen eikä arvoihin. Käydään läpi, mitä listauksen 20-35 kuvion osat tarkoittavat; täydellisestä makrokuviosyntaksista katso Rustin viite.

Ensin käytämme sulkeita koko kuvion ympärillä. Käytämme dollarimerkkiä ($) julistaaksemme muuttujan makrojärjestelmässä, joka sisältää kuvioon täsmäävän Rust-koodin. Dollarimerkki tekee selväksi, että kyseessä on makromuuttuja eikä tavallinen Rust-muuttuja. Seuraavaksi tulee sulkeet, jotka sieppaavat kuvioon täsmäävät arvot käytettäväksi korvaavassa koodissa. $()-sisällä on $x:expr, joka täsmää mihin tahansa Rust-lausekkeeseen ja antaa lausekkeelle nimen $x.

$()-jälkeinen pilkku osoittaa, että jokaisen $()-sisällön täsmäävän koodin instanssin välissä täytyy olla kirjaimellinen pilkkuerotin. * määrittää, että kuvio täsmää nolla tai useampaan edeltävään osaan.

Kun kutsumme tätä makroa vec![1, 2, 3];-kutsulla, $x-kuvio täsmää kolme kertaa lausekkeisiin 1, 2 ja 3.

Katsotaan nyt tähän haaraan liittyvän rungon kuviota: temp_vec.push() $()*-sisällä generoidaan jokaiselle $()-kuvioon täsmäävälle osalle nolla tai useamman kerran riippuen siitä, montako kertaa kuvio täsmää. $x korvataan jokaisella täsmäävällä lausekkeella. Kun kutsumme tätä makroa vec![1, 2, 3];-kutsulla, tämän makrokutsun korvaava generoitu koodi on seuraava:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

Olemme määritelleet makron, joka voi ottaa minkä tahansa määrän argumentteja minkä tahansa tyypillä ja generoida koodin vektorin luomiseksi määritellyillä elementeillä.

Lisätietoa makrojen kirjoittamisesta löydät verkkodokumentaatiosta tai muista lähteistä, kuten Daniel Keepin aloittamasta ja Lukas Wirthin jatkamasta ”The Little Book of Rust Macros” -oppaasta.

Proseduraaliset makrot koodin generointiin attribuuteista

Makrojen toinen muoto on proseduraalinen makro, joka toimii enemmän kuin funktio (ja on eräänlaista proseduuria). Proseduraaliset makrot ottavat koodia syötteenä, käsittelevät sitä ja tuottavat koodia tulosteena sen sijaan, että täsmäisivät kuvioihin ja korvaisivat koodin muulla koodilla kuten deklaratiiviset makrot. Kolme proseduraalisen makron lajia ovat mukautettu derive, attribuuttimainen ja funktiomainen, ja ne kaikki toimivat samankaltaisesti.

Proseduraalisia makroja luodessa määritelmien täytyy sijaita omassa kratessaan erityisellä kratetyypillä. Tämä johtuu monimutkaisista teknisistä syistä, joita toivomme poistavamme tulevaisuudessa. Listauksessa 20-36 näytämme, miten proseduraalinen makro määritellään, jossa some_attribute on paikkamerkki tietyn makrolajin käytölle.

Filename: src/lib.rs
use proc_macro::TokenStream;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-36: Esimerkki proseduraalisen makron määrittelystä

Proseduraalisen makron määrittelevä funktio ottaa TokenStream-syötteen ja tuottaa TokenStream-tulosteen. TokenStream-tyyppi on määritelty Rustin mukana tulevassa proc_macro-kratessa ja edustaa tokenien jonoa. Tämä on makron ydin: makron käsittelemä lähdekoodi muodostaa syöte-TokenStream-arvon, ja makron tuottama koodi on tuloste-TokenStream. Funktioon on liitetty myös attribuutti, joka määrittää, minkä lajista proseduraalista makroa luomme. Samassa kratessa voi olla useita proseduraalisen makron lajeja.

Katsotaan eri proseduraalisen makron lajeja. Aloitamme mukautetulla derive-makrolla ja selitämme sitten pienet erot, jotka tekevät muista lajeista erilaisia.

Mukautetut derive-makrot

Luodaan krate nimeltä hello_macro, joka määrittelee traitin nimeltä HelloMacro yhdellä assosioituneella funktiolla nimeltä hello_macro. Sen sijaan, että pakottaisimme käyttäjät toteuttamaan HelloMacro-traitin jokaiselle tyypeilleen, tarjoamme proseduraalisen makron, jotta käyttäjät voivat merkitä tyypin attribuutilla #[derive(HelloMacro)] saadakseen oletustoteutuksen hello_macro-funktiolle. Oletustoteutus tulostaa Hello, Macro! My name is TypeName!, jossa TypeName on tyyppi, jolle trait on määritelty. Toisin sanoen kirjoitamme kraten, jonka avulla toinen ohjelmoija voi kirjoittaa listauksen 20-37 kaltaista koodia käyttäen kratettamme.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-37/src/main.rs}}
Listing 20-37: Koodi, jonka kratettamme käyttäjä voi kirjoittaa proseduraalista makroamme käyttäen

Tämä koodi tulostaa Hello, Macro! My name is Pancakes!, kun olemme valmiita. Ensimmäinen askel on luoda uusi kirjastokrate näin:

$ cargo new hello_macro --lib

Seuraavaksi listauksessa 20-38 määrittelemme HelloMacro-traitin ja sen assosioituneen funktion.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-38/hello_macro/src/lib.rs}}
Listing 20-38: Yksinkertainen trait, jota käytämme derive-makron kanssa

Meillä on trait ja sen funktio. Tässä vaiheessa kratettamme käyttäjä voisi toteuttaa traitin saavuttaakseen halutun toiminnallisuuden, kuten listauksessa 20-39.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-39/pancakes/src/main.rs}}
Listing 20-39: Miltä näyttäisi, jos käyttäjät kirjoittaisivat HelloMacro-traitin manuaalisen toteutuksen

Heidän täytyisi kuitenkin kirjoittaa toteutuslohko jokaiselle tyypille, jota haluavat käyttää hello_macro-funktion kanssa; haluamme säästää heidät tältä työltä.

Lisäksi emme voi vielä tarjota hello_macro-funktiolle oletustoteutusta, joka tulostaisi tyypin nimen, jolle trait on toteutettu: Rustilla ei ole reflektio-ominaisuuksia, joten se ei voi hakea tyypin nimeä ajonaikana. Tarvitsemme makron generoimaan koodia käännösaikana.

Seuraava askel on määritellä proseduraalinen makro. Kirjoitushetkellä proseduraalisten makrojen täytyy olla omassa kratessaan. Tämä rajoitus saattaa poistua tulevaisuudessa. Kratien ja makrokratien rakentelun käytäntö on seuraava: kratelle nimeltä foo mukautettu derive-proseduraalinen makrokrate kutsutaan foo_derive. Aloitetaan uusi krate nimeltä hello_macro_derive hello_macro-projektimme sisällä:

$ cargo new hello_macro_derive --lib

Kaksi kratettamme ovat tiiviisti sidoksissa, joten luomme proseduraalisen makrokrateen hello_macro-kraten hakemistoon. Jos muutamme hello_macro-kraten trait-määritelmää, meidän täytyy muuttaa myös hello_macro_derive-kraten proseduraalisen makron toteutusta. Kaksi kratetta täytyy julkaista erikseen, ja näitä kratteja käyttävien ohjelmoijien täytyy lisätä molemmat riippuvuuksiksi ja tuoda ne näkyvyysalueelle. Voisimme sen sijaan tehdä hello_macro-kratesta riippuvaisen hello_macro_derive-kratesta ja uudelleenviedä proseduraalisen makron koodin. Tämä projektirakenne kuitenkin mahdollistaa hello_macro-kraten käytön, vaikka käyttäjä ei haluaisi derive-toiminnallisuutta.

Meidän täytyy julistaa hello_macro_derive-krate proseduraaliseksi makrokrateeksi. Tarvitsemme myös toiminnallisuutta syn- ja quote-krateista, kuten näet pian, joten meidän täytyy lisätä ne riippuvuuksiksi. Lisää seuraava hello_macro_derive-kraten Cargo.toml-tiedostoon:

Filename: hello_macro_derive/Cargo.toml
{{#include ../listings/ch20-advanced-features/listing-20-40/hello_macro/hello_macro_derive/Cargo.toml:6:12}}

Aloittaaksesi proseduraalisen makron määrittelyn, laita listauksen 20-40 koodi hello_macro_derive-kraten src/lib.rs-tiedostoon. Huomaa, että tämä koodi ei kääntyisi ennen kuin lisäät määritelmän impl_hello_macro-funktiolle.

Filename: hello_macro_derive/src/lib.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-40/hello_macro/hello_macro_derive/src/lib.rs}}
Listing 20-40: Koodi, jota useimmat proseduraaliset makrokratet tarvitsevat Rust-koodin käsittelyyn

Huomaa, että olemme jakaneet koodin hello_macro_derive-funktioon, joka vastaa TokenStream-arvon jäsentämisestä, ja impl_hello_macro-funktioon, joka vastaa syntaksipuun muuntamisesta: tämä tekee proseduraalisen makron kirjoittamisesta kätevämpää. Ulomman funktion (hello_macro_derive tässä tapauksessa) koodi on sama lähes jokaisessa näkemässäsi tai luomassasi proseduraalisessa makrokrateessa. Sisemmän funktion (impl_hello_macro tässä tapauksessa) rungon koodi on erilainen proseduraalisen makron tarkoituksen mukaan.

Olemme tuoneet kolme uutta kratetta: proc_macro, syn ja quote. proc_macro-krate tulee Rustin mukana, joten emme tarvinneet lisätä sitä Cargo.toml-tiedoston riippuvuuksiin. proc_macro-krate on kääntäjän API, jonka avulla voimme lukea ja käsitellä Rust-koodia omasta koodistamme.

syn-krate jäsentää Rust-koodia merkkijonosta tietorakenteeksi, jolla voimme suorittaa operaatioita. quote-krate muuntaa syn-tietorakenteet takaisin Rust-koodiksi. Nämä kratet tekevät paljon yksinkertaisemmaksi jäsentää mitä tahansa Rust-koodia, jota haluamme käsitellä: täydellisen Rust-koodin parserin kirjoittaminen ei ole yksinkertainen tehtävä.

hello_macro_derive-funktiota kutsutaan, kun kirjastomme käyttäjä määrittää #[derive(HelloMacro)] tyypille. Tämä on mahdollista, koska olemme merkinneet hello_macro_derive-funktion proc_macro_derive-attribuutilla ja määrittäneet nimen HelloMacro, joka vastaa traitimme nimeä; tämä on käytäntö, jota useimmat proseduraaliset makrot noudattavat.

hello_macro_derive-funktio muuntaa ensin input-arvon TokenStream-tyypistä tietorakenteeksi, jota voimme sitten tulkita ja käsitellä. Tässä syn tulee mukaan. syn-kraten parse-funktio ottaa TokenStream-arvon ja palauttaa DeriveInput-rakenteen, joka edustaa jäsennettyä Rust-koodia. Listaus 20-41 näyttää oleelliset osat DeriveInput-rakenteesta, jonka saamme jäsentämällä merkkijonon struct Pancakes;.

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}
Listing 20-41: DeriveInput-instanssi, jonka saamme jäsentäessämme listauksen 20-37 makroattribuutilla varustetun koodin

Tämän rakenteen kentät osoittavat, että jäsentämämme Rust-koodi on yksikkörakenne, jonka ident (identifier, eli nimi) on Pancakes. Rakenteessa on lisää kenttiä kaikenlaisen Rust-koodin kuvaamiseen; katso syn-dokumentaatiosta DeriveInput lisätietoja.

Määrittelemme pian impl_hello_macro-funktion, jossa rakennamme uutta Rust-koodia, jonka haluamme sisällyttää. Mutta ennen sitä huomaa, että derive-makromme tuloste on myös TokenStream. Palautettu TokenStream lisätään kratettamme käyttäjien kirjoittamaan koodiin, joten kun he kääntävät kratensa, he saavat lisätoiminnallisuuden, jonka tarjoamme muokatussa TokenStream-arvossa.

Saatoit huomata, että kutsumme unwrap-funktiota aiheuttaaksemme hello_macro_derive-funktion panikoinnin, jos syn::parse-funktion kutsu epäonnistuu. Proseduraalisen makromme täytyy panikoida virheissä, koska proc_macro_derive-funktioiden täytyy palauttaa TokenStream eikä Result proseduraalisen makro-API:n mukaisesti. Olemme yksinkertaistaneet tämän esimerkin käyttämällä unwrap-funktiota; tuotantokoodissa sinun pitäisi antaa tarkempia virheilmoituksia siitä, mikä meni pieleen, käyttämällä panic!- tai expect-makroa.

Nyt kun meillä on koodi, joka muuntaa annotoidun Rust-koodin TokenStream-arvosta DeriveInput-instanssiksi, generoidaan koodi, joka toteuttaa HelloMacro-traitin annotoidulle tyypille, kuten listauksessa 20-42.

Filename: hello_macro_derive/src/lib.rs
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-42/hello_macro/hello_macro_derive/src/lib.rs:here}}
Listing 20-42: HelloMacro-traitin toteutus jäsennetyn Rust-koodin avulla

Saamme Ident-rakenteen instanssin, joka sisältää annotoidun tyypin nimen (tunnisteen), käyttämällä ast.ident. Listauksen 20-41 rakenne osoittaa, että kun ajamme impl_hello_macro-funktion listauksen 20-37 koodilla, saamamme ident sisältää ident-kentän arvolla "Pancakes". Näin ollen listauksen 20-42 name-muuttuja sisältää Ident-rakenteen instanssin, joka tulostettuna on merkkijono "Pancakes", listauksen 20-37 rakenteen nimi.

quote!-makro sallii määritellä Rust-koodin, jonka haluamme palauttaa. Kääntäjä odottaa jotain erilaista kuin quote!-makron suoran suorituksen tulos, joten meidän täytyy muuntaa se TokenStream-tyypiksi. Teemme tämän kutsumalla into-metodia, joka kuluttaa tämän välisen esityksen ja palauttaa vaaditun TokenStream-tyypin arvon.

quote!-makrossa on myös erittäin hienoja mallipohjamekanismeja: voimme kirjoittaa #name, ja quote! korvaa sen name-muuttujan arvolla. Voit jopa tehdä toistoa samankaltaisesti kuin tavallisissa makroissa. Katso quote-kraten dokumentaatiosta perusteellinen johdanto.

Haluamme proseduraalisen makromme generoivan HelloMacro-traitin toteutuksen käyttäjän annotoimalle tyypille, jonka saamme #name-avulla. Trait-toteutuksessa on yksi funktio hello_macro, jonka runko sisältää haluamamme toiminnallisuuden: tulostaa Hello, Macro! My name is ja sitten annotoidun tyypin nimen.

Tässä käytetty stringify!-makro on sisäänrakennettu Rustiin. Se ottaa Rust-lausekkeen, kuten 1 + 2, ja muuntaa lausekkeen käännösaikana merkkijonoliteraaliksi, kuten "1 + 2". Tämä eroaa format!- tai println!-makroista, jotka arvioivat lausekkeen ja muuntavat tuloksen String-tyypiksi. On mahdollista, että #name-syöte on tulostettava lauseke, joten käytämme stringify!-makroa. stringify!-makron käyttö säästää myös allokaation muuntamalla #name:n merkkijonoliteraaliksi käännösaikana.

Tässä vaiheessa cargo build pitäisi onnistua sekä hello_macro- että hello_macro_derive-krateissa. Kytketään nämä kratet listauksen 20-37 koodiin ja katsotaan proseduraalista makroa käytännössä! Luo uusi binääriprojekti projects-hakemistoosi komennolla cargo new pancakes. Meidän täytyy lisätä hello_macro ja hello_macro_derive riippuvuuksiksi pancakes-kraten Cargo.toml-tiedostoon. Jos julkaiset hello_macro- ja hello_macro_derive-versiosi crates.io -sivustolle, ne olisivat tavallisia riippuvuuksia; jos et, voit määrittää ne path-riippuvuuksiksi seuraavasti:

{{#include ../listings/ch20-advanced-features/no-listing-21-pancakes/pancakes/Cargo.toml:6:8}}

Laita listauksen 20-37 koodi src/main.rs-tiedostoon ja aja cargo run: sen pitäisi tulostaa Hello, Macro! My name is Pancakes!. Proseduraalisen makron HelloMacro-trait-toteutus sisällytettiin ilman, että pancakes-kraten täytyi toteuttaa sitä; #[derive(HelloMacro)] lisäsi trait-toteutuksen.

Seuraavaksi tutkitaan, miten muut proseduraalisen makron lajit eroavat mukautetuista derive-makroista.

Attribuuttimaiset makrot

Attribuuttimaiset makrot muistuttavat mukautettuja derive-makroja, mutta sen sijaan, että ne generoisivat koodia derive-attribuutille, ne sallivat uusien attribuuttien luomisen. Ne ovat myös joustavampia: derive toimii vain rakenteille ja luettelotyypeille; attribuutteja voi käyttää myös muissa kohteissa, kuten funktioissa. Tässä on esimerkki attribuuttimaisen makron käytöstä. Oletetaan, että sinulla on route-attribuutti, joka annotoi funktioita web-sovelluskehyksen käytössä:

#[route(GET, "/")]
fn index() {

Tämän #[route]-attribuutin määrittelisi kehys proseduraalisena makrona. Makromääritelmäfunktion signatuuri näyttäisi tältä:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Tässä meillä on kaksi TokenStream-tyyppistä parametria. Ensimmäinen on attribuutin sisältö: GET, "/" -osa. Toinen on kohteen runko, johon attribuutti on liitetty: tässä tapauksessa fn index() {} ja funktion rungon loppuosa.

Muuten attribuuttimaiset makrot toimivat samalla tavalla kuin mukautetut derive-makrot: luot kraten proc-macro-kratetyypillä ja toteutat funktion, joka generoi haluamasi koodin!

Funktiomaiset makrot

Funktiomaiset makrot määrittelevät makroja, jotka näyttävät funktiokutsuilta. Samankaltaisesti kuin macro_rules!-makrot, ne ovat joustavampia kuin funktiot; esimerkiksi ne voivat ottaa tuntemattoman määrän argumentteja. macro_rules!-makroja voi kuitenkin määritellä vain aiemmin käsitellyllä match-tyylisellä syntaksilla ”Deklaratiiviset makrot yleiseen metaprogrammointiin” -osiossa. Funktiomaiset makrot ottavat TokenStream-parametrin, ja niiden määritelmä käsittelee sitä Rust-koodilla kuten muutkin proseduraalisen makron lajit. Esimerkki funktiomaisesta makrosta on sql!-makro, jota voisi kutsua näin:

let sql = sql!(SELECT * FROM posts WHERE id=1);

Tämä makro jäsentäisi sen sisällä olevan SQL-lausekkeen ja tarkistaisi sen syntaktisen oikeellisuuden, mikä on paljon monimutkaisempaa käsittelyä kuin macro_rules!-makro pystyy tekemään. sql!-makro määriteltäisiin näin:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Tämä määritelmä muistuttaa mukautetun derive-makron signatuuria: saamme sulkeiden sisällä olevat tokenit ja palautamme koodin, jonka haluamme generoida.

Yhteenveto

Huh! Nyt sinulla on Rust-ominaisuuksia työkalupakissasi, joita et todennäköisesti käytä usein, mutta tiedät niiden olevan saatavilla hyvin erityisissä tilanteissa. Olemme esitelleet useita monimutkaisia aiheita, jotta kun kohtaat niitä virheilmoitusten ehdotuksissa tai muiden koodissa, tunnistat nämä käsitteet ja syntaksin. Käytä tätä lukua viitteenä ratkaisujen löytämiseen.

Seuraavaksi laitamme kaiken, mistä olemme puhuneet koko kirjan ajan, käytäntöön ja teemme vielä yhden projektin!

Loppuprojekti: Verkkopalvelimen rakentaminen Rustilla

Tässä luvussa rakennamme yksinkertaisen verkkopalvelimen käyttäen Rustia. Tämä projekti yhdistää monia aiemmin opittuja Rustin ominaisuuksia, kuten:

  • I/O-käsittely: Lukeminen ja kirjoittaminen tiedostoihin sekä verkkopyyntöjen käsittely.
  • Rinnakkaisuus: Verkkopalvelin käsittelee useita pyyntöjä samanaikaisesti.
  • Ohjelmoinnin rakenteet: Hyödynnämme rakenteita, traitteja ja sulkeisia tehokkaan ja ylläpidettävän koodin luomiseksi.

Tavoitteemme on rakentaa yksinkertainen verkkopalvelin, joka pystyy palvelemaan staattisia HTML-tiedostoja ja käsittelemään useita yhteyksiä samanaikaisesti. Tämä antaa käytännön kokemusta Rustin tehokkuudesta ja turvallisuudesta verkkosovelluksissa.

Seuraavaksi aloitamme palvelimen rakentamisen vaihe vaiheelta.

Yksisäikeisen verkkopalvelimen rakentaminen

Yksisäikeisen verkkopalvelimen rakentaminen

Aloitamme saamalla yksisäikeisen verkkopalvelimen toimimaan. Ennen kuin aloitamme, katsotaan nopea yleiskatsaus verkkopalvelimien rakentamiseen liittyvistä protokollista. Näiden protokollien yksityiskohdat ylittävät tämän kirjan laajuuden, mutta lyhyt yleiskatsaus antaa tarvitsemasi tiedon.

Verkkopalvelimiin liittyvät kaksi pääprotokollaa ovat Hypertext Transfer Protocol (HTTP) ja Transmission Control Protocol (TCP). Molemmat protokollat ovat pyyntö-vastaus-protokollia, mikä tarkoittaa, että asiakas aloittaa pyynnöt ja palvelin kuuntelee pyyntöjä ja antaa vastauksen asiakkaalle. Näiden pyyntöjen ja vastausten sisältö määritellään protokollien toimesta.

TCP on alemmantasoinen protokolla, joka kuvaa yksityiskohdat siitä, miten tieto siirtyy palvelimelta toiselle, mutta ei määrittele, mitä tämä tieto on. HTTP rakentuu TCP:n päälle määrittelemällä pyyntöjen ja vastausten sisällön. Teknisesti HTTP on mahdollista käyttää muiden protokollien kanssa, mutta valtavassa enemmistössä tapauksista HTTP lähettää datansa TCP:n yli. Työskentelemme TCP:n ja HTTP-pyyntöjen ja -vastausten raakatavujen kanssa.

TCP-yhteyden kuunteleminen

Verkkopalvelimemme täytyy kuunnella TCP-yhteyttä, joten se on ensimmäinen osa, jota työstämme. Standardikirjasto tarjoaa std::net-moduulin, joka antaa meille mahdollisuuden tehdä tämän. Tehdään uusi projekti tavalliseen tapaan:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

Syötä nyt listauksen 21-1 koodi src/main.rs-tiedostoon aloittaaksesi. Tämä koodi kuuntelee paikallista osoitetta 127.0.0.1:7878 saapuvia TCP-streameja varten. Kun se saa saapuvan streamin, se tulostaa Connection established!.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-01/src/main.rs}}
}
Listing 21-1: Saapuvien streamien kuunteleminen ja viestin tulostaminen streamin vastaanotosta

TcpListener-rakenteen avulla voimme kuunnella TCP-yhteyksiä osoitteessa 127.0.0.1:7878. Osoitteessa kaksoispisteen edeltävä osa on IP-osoite, joka edustaa tietokonettasi (tämä on sama jokaisella tietokoneella eikä edusta kirjoittajien tietokonetta erityisesti), ja 7878 on portti. Olemme valinneet tämän portin kahdesta syystä: HTTP:tä ei yleensä hyväksytä tällä portilla, joten palvelimemme ei todennäköisesti ole ristiriidassa muiden koneellasi mahdollisesti ajavien verkkopalvelimien kanssa, ja 7878 on rust kirjoitettuna puhelinnäppäimistöllä.

Tässä skenaariossa bind-funktio toimii kuten new-funktio siinä mielessä, että se palauttaa uuden TcpListener-instanssin. Funktiota kutsutaan bind-nimiseksi, koska verkkoyhteyksissä porttiin yhdistämistä kuuntelemista varten kutsutaan ”porttiin sitomiseksi” (binding to a port).

bind-funktio palauttaa Result<T, E>-arvon, mikä osoittaa, että sitominen voi epäonnistua — esimerkiksi jos ajamme kaksi ohjelman instanssia ja siten kaksi ohjelmaa kuuntelee samaa porttia. Koska kirjoitamme peruspalvelimen vain oppimistarkoituksiin, emme huolehdi tällaisten virheiden käsittelystä; sen sijaan käytämme unwrap-metodia lopettaaksemme ohjelman, jos virheitä tapahtuu.

TcpListener-rakenteen incoming-metodi palauttaa iteraattorin, joka antaa meille streamien sarjan (tarkemmin sanottuna TcpStream-tyyppisten streamien). Yksittäinen stream edustaa avointa yhteyttä asiakkaan ja palvelimen välillä. Yhteys on nimi koko pyyntö-vastaus-prosessille, jossa asiakas yhdistää palvelimeen, palvelin tuottaa vastauksen ja palvelin sulkee yhteyden. Näin ollen luemme TcpStream-rakenteesta nähdäksemme, mitä asiakas lähetti, ja kirjoitamme sitten vastauksemme streamiin lähettääksemme dataa takaisin asiakkaalle. Kaiken kaikkiaan tämä for-silmukka käsittelee jokaisen yhteyden vuorollaan ja tuottaa meille streamien sarjan käsiteltäväksi.

Tällä hetkellä streamin käsittelymme koostuu unwrap-metodin kutsumisesta lopettaaksemme ohjelman, jos streamissä on virheitä; jos virheitä ei ole, ohjelma tulostaa viestin. Lisäämme lisää toiminnallisuutta onnistumistapaukselle seuraavassa listauksessa. Syy, miksi saatamme saada virheitä incoming-metodista, kun asiakas yhdistää palvelimeen, on se, että emme itse asiassa iteroi yhteyksien yli. Sen sijaan iteromme yhteysyritysten yli. Yhteys ei välttämättä onnistu monesta syystä, joista monet ovat käyttöjärjestelmäkohtaisia. Esimerkiksi monilla käyttöjärjestelmillä on raja samanaikaisesti avoimien yhteyksien määrälle; uudet yhteysyritykset tämän rajan yli tuottavat virheen, kunnes jotkut avoimista yhteyksistä suljetaan.

Kokeillaan ajaa tämä koodi! Kutsu cargo run -komentoa terminaalissa ja lataa sitten 127.0.0.1:7878 verkkoselaimessa. Selaimen pitäisi näyttää virheilmoitus, kuten ”Connection reset”, koska palvelin ei tällä hetkellä lähetä mitään dataa takaisin. Mutta kun katsot terminaaliasi, sinun pitäisi nähdä useita viestejä, jotka tulostettiin, kun selain yhdisti palvelimeen!

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

Joskus näet useita viestejä tulostettuna yhdestä selainpyynnöstä; syy voi olla se, että selain tekee pyynnön sivulle sekä pyynnön muille resursseille, kuten favicon.ico-kuvakkeelle, joka näkyy selaimen välilehdessä.

Voi myös olla, että selain yrittää yhdistää palvelimeen useita kertoja, koska palvelin ei vastaa millään datalla. Kun stream menee näkyvyysalueen ulkopuolelle ja pudotetaan silmukan lopussa, yhteys suljetaan osana drop-toteutusta. Selaimet käsittelevät joskus suljettuja yhteyksiä yrittämällä uudelleen, koska ongelma saattaa olla tilapäinen.

Selaimet avaavat joskus useita yhteyksiä palvelimeen lähettämättä pyyntöjä, jotta jos ne myöhemmin lähettävät pyyntöjä, ne voivat tapahtua nopeammin. Kun näin tapahtuu, palvelimemme näkee jokaisen yhteyden riippumatta siitä, lähetetäänkö kyseisen yhteyden yli pyyntöjä. Monet Chrome-pohjaiset selaimet tekevät näin; voit poistaa tämän optimoinnin käytöstä yksityisen selaamisen tilassa tai käyttämällä toista selainta.

Tärkeä tekijä on, että olemme onnistuneesti saaneet käsitteen TCP-yhteydestä!

Muista pysäyttää ohjelma painamalla ctrl-C, kun olet valmis tietyn koodiversion ajamisen kanssa. Käynnistä sitten ohjelma uudelleen kutsumalla cargo run -komentoa jokaisen koodimuutoksen jälkeen varmistaaksesi, että ajat uusinta koodia.

Pyynnön lukeminen

Toteutetaan toiminnallisuus pyynnön lukemiseksi selaimesta! Erottaaksemme ensin yhteyden saamisen ja sitten toimenpiteen tekemisen yhteydellä, aloitamme uuden funktion yhteyksien käsittelyyn. Tässä uudessa handle_connection-funktiossa luemme dataa TCP-streamistä ja tulostamme sen nähdäksemme selaimen lähettämän datan. Muuta koodi näyttämään listaukselta 21-2.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-02/src/main.rs}}
}
Listing 21-2: Lukeminen TcpStream-rakenteesta ja datan tulostaminen

Tuomme std::io::prelude- ja std::io::BufReader-moduulit näkyvyysalueelle saadaksemme käyttöön traitit ja tyypit, joiden avulla voimme lukea streamistä ja kirjoittaa siihen. main-funktion for-silmukassa sen sijaan, että tulostaisimme viestin siitä, että saimme yhteyden, kutsumme nyt uutta handle_connection-funktiota ja välitämme sille stream-parametrin.

handle_connection-funktiossa luomme uuden BufReader-instanssin, joka käärii viitteen streamiin. BufReader lisää puskurointia hallitsemalla std::io::Read-traitin metodikutsuja puolestamme.

Luomme muuttujan nimeltä http_request kerätäksemme selaimen palvelimellemme lähettämän pyynnön rivit. Ilmaisemme, että haluamme kerätä nämä rivit vektoriin lisäämällä Vec<_>-tyyppiannotaation.

BufReader toteuttaa std::io::BufRead-traitin, joka tarjoaa lines-metodin. lines-metodi palauttaa iteraattorin tyyppiä Result<String, std::io::Error> jakamalla datastreamin aina kun se näkee rivinvaihtotavun. Saadaksemme jokaisen String-arvon, käytämme map- ja unwrap-metodeja jokaiselle Result-arvolle. Result voi olla virhe, jos data ei ole kelvollista UTF-8:aa tai jos streamin lukemisessa oli ongelma. Taaskin tuotantoohjelman pitäisi käsitellä nämä virheet tyylikkäämmin, mutta valitsemme yksinkertaisuuden vuoksi lopettaa ohjelman virhetapauksessa.

Selain ilmaisee HTTP-pyynnön päättymisen lähettämällä kaksi rivinvaihtomerkkiä peräkkäin, joten saadaksemme yhden pyynnön streamistä otamme rivejä, kunnes saamme rivin, joka on tyhjä merkkijono. Kun olemme keränneet rivit vektoriin, tulostamme ne käyttämällä kaunista debug-muotoilua nähdäksemme, mitä ohjeita verkkoselain lähettää palvelimellemme.

Kokeillaan tätä koodia! Käynnistä ohjelma ja tee pyyntö verkkoselaimessa uudelleen. Huomaa, että saamme edelleen virhesivun selaimessa, mutta ohjelman tulosteen terminaalissa pitäisi nyt näyttää suunnilleen tältä:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

Selaimestasi riippuen saatat saada hieman erilaisen tulosteen. Nyt kun tulostamme pyyntödataa, voimme nähdä, miksi saamme useita yhteyksiä yhdestä selainpyynnöstä katsomalla polkua GET-rivin jälkeen. Jos toistuvat yhteydet pyytävät kaikki /, tiedämme, että selain yrittää hakea / toistuvasti, koska se ei saa vastausta ohjelmastamme.

Puretaan tämä pyyntödata ymmärtääksemme, mitä selain pyytää ohjelmaltamme.

Tarkempi katsaus HTTP-pyyntöön

HTTP on tekstipohjainen protokolla, ja pyyntö on tässä muodossa:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

Ensimmäinen rivi on pyyntörivi, joka sisältää tietoa siitä, mitä asiakas pyytää. Pyyntörivin ensimmäinen osa ilmaisee käytetyn metodin, kuten GET tai POST, joka kuvaa, miten asiakas tekee tämän pyynnön. Asiakkaamme käytti GET-pyyntöä, mikä tarkoittaa, että se pyytää tietoa.

Pyyntörivin seuraava osa on /, joka ilmaisee uniform resource identifierin (URI), jota asiakas pyytää: URI on melkein, mutta ei aivan, sama kuin uniform resource locator (URL). Ero URI:n ja URL:n välillä ei ole tärkeä tämän luvun tarkoituksiin, mutta HTTP-spesifikaatio käyttää termiä URI, joten voimme ajatella URL:n sijasta URI:ta.

Viimeinen osa on asiakkaan käyttämä HTTP-versio, ja sitten pyyntörivi päättyy CRLF-sekvenssiin. (CRLF tulee sanoista carriage return ja line feed, jotka ovat peräisin kirjoituskonetyöaikojen termeistä!) CRLF-sekvenssi voidaan kirjoittaa myös muodossa \r\n, missä \r on carriage return ja \n on line feed. CRLF-sekvenssi erottaa pyyntörivin pyynnön lopusta datasta. Huomaa, että kun CRLF tulostetaan, näemme uuden rivin alkavan \r\n:n sijaan.

Katsomalla pyyntörividataa, jonka saimme ohjelman ajosta tähän asti, näemme, että GET on metodi, / on pyynnön URI ja HTTP/1.1 on versio.

Pyyntörivin jälkeen jäljellä olevat rivit alkaen Host:-rivistä eteenpäin ovat otsakkeita. GET-pyynnöillä ei ole runkoa.

Kokeile tehdä pyyntö eri selaimesta tai pyytää eri osoitetta, kuten 127.0.0.1:7878/test, nähdäksesi, miten pyyntödata muuttuu.

Nyt kun tiedämme, mitä selain pyytää, lähetetään takaisin dataa!

Vastauksen kirjoittaminen

Toteutamme datan lähettämisen asiakkaan pyyntöön vastaamiseksi. Vastaukset ovat seuraavassa muodossa:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

Ensimmäinen rivi on tilarivi, joka sisältää vastauksessa käytetyn HTTP-version, numeerisen tilakoodin, joka tiivistää pyynnön tuloksen, ja syy-lausekkeen, joka antaa tekstikuvauksen tilakoodista. CRLF-sekvenssin jälkeen tulevat otsakkeet, toinen CRLF-sekvenssi ja vastauksen runko.

Tässä on esimerkkivastaus, joka käyttää HTTP-versiota 1.1, tilakoodia 200, syy-lauseketta OK, ei otsakkeita eikä runkoa:

HTTP/1.1 200 OK\r\n\r\n

Tilakoodi 200 on standardi onnistumisvastaus. Teksti on pieni onnistunut HTTP-vastaus. Kirjoitetaan tämä streamiin vastaukseksemme onnistuneeseen pyyntöön! handle_connection-funktiosta poistetaan println!, joka tulosti pyyntödataa, ja korvataan se listauksen 21-3 koodilla.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-03/src/main.rs:here}}
}
Listing 21-3: Pienen onnistuneen HTTP-vastauksen kirjoittaminen streamiin

Ensimmäinen uusi rivi määrittelee response-muuttujan, joka sisältää onnistumisviestin datan. Sitten kutsumme as_bytes-metodia response-muuttujalla muuntaaksemme merkkijonodatan tavuiksi. stream-rakenteen write_all-metodi ottaa &[u8]-tyypin ja lähettää nämä tavut suoraan yhteyden kautta. Koska write_all-operaatio voi epäonnistua, käytämme unwrap-metodia virhetuloksen kohdalla kuten aiemmin. Taaskin todellisessa sovelluksessa lisäisit virheenkäsittelyn tähän.

Näillä muutoksilla ajetaan koodimme ja tehdään pyyntö. Emme enää tulosta mitään dataa terminaaliin, joten emme näe mitään tulostetta muuten kuin Cargolta. Kun lataat 127.0.0.1:7878 verkkoselaimessa, sinun pitäisi saada tyhjä sivu virheen sijaan. Olet juuri käsin koodannut HTTP-pyynnön vastaanottamisen ja vastauksen lähettämisen!

Oikean HTML:n palauttaminen

Toteutetaan toiminnallisuus enemmän kuin tyhjän sivun palauttamiseksi. Luo uusi tiedosto hello.html projektihakemistosi juureen, ei src-hakemistoon. Voit syöttää mitä tahansa HTML:ää; listausta 21-4 näyttää yhden mahdollisuuden.

Filename: hello.html
{{#include ../listings/ch21-web-server/listing-21-05/hello.html}}
Listing 21-4: Esimerkki-HTML-tiedosto vastauksessa palautettavaksi

Tämä on minimaalinen HTML5-dokumentti otsikolla ja tekstillä. Palauttaaksemme tämän palvelimelta pyynnön vastaanotosta, muokkaamme handle_connection-funktiota kuten listauksessa 21-5 näytetään lukemaan HTML-tiedosto, lisäämään se vastauksen rungoksi ja lähettämään sen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-05/src/main.rs:here}}
}
Listing 21-5: hello.html-tiedoston sisällön lähettäminen vastauksen rungoksi

Olemme lisänneet fs-moduulin use-lauseeseen tuodaksemme standardikirjaston tiedostojärjestelmämoduulin näkyvyysalueelle. Koodin tiedoston sisällön lukemiseksi merkkijonoon pitäisi näyttää tutulta; käytimme sitä lukiessamme tiedoston sisältöä I/O-projektissamme listauksessa 12-4.

Seuraavaksi käytämme format!-makroa lisätäksemme tiedoston sisällön onnistumisvastauksen rungoksi. Varmistaaksemme kelvollisen HTTP-vastauksen lisäämme Content-Length-otsakkeen, joka on asetettu vastausrukkimme kooksi, tässä tapauksessa hello.html-tiedoston kooksi.

Aja tämä koodi cargo run -komennolla ja lataa 127.0.0.1:7878 selaimessasi; sinun pitäisi nähdä HTML renderöitynä!

Tällä hetkellä sivuutamme http_request-muuttujan pyyntödatan ja lähetämme HTML-tiedoston sisällön takaisin ehdottomasti. Tämä tarkoittaa, että jos yrität pyytää 127.0.0.1:7878/something-else selaimessasi, saat silti saman HTML-vastauksen. Tällä hetkellä palvelimemme on hyvin rajallinen eikä tee sitä, mitä useimmat verkkopalvelimet tekevät. Haluamme mukauttaa vastauksiamme pyynnön mukaan ja lähettää HTML-tiedoston takaisin vain hyvin muotoillulle pyynnölle osoitteeseen /.

Pyynnön validointi ja valikoiva vastaaminen

Tällä hetkellä verkkopalvelimemme palauttaa tiedoston HTML:n riippumatta siitä, mitä asiakas pyysi. Lisätään toiminnallisuus tarkistaaksemme, pyytääkö selain /, ennen kuin palautamme HTML-tiedoston, ja palautamme virheen, jos selain pyytää jotain muuta. Tätä varten meidän täytyy muokata handle_connection-funktiota, kuten listauksessa 21-6. Tämä uusi koodi tarkistaa vastaanotetun pyynnön sisällön sitä vastaan, miltä pyyntö osoitteeseen / näyttää, ja lisää if- ja else-lohkot käsittelemään pyyntöjä eri tavoin.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-06/src/main.rs:here}}
}
Listing 21-6: Pyyntöjen käsittely osoitteeseen / eri tavalla kuin muiden pyyntöjen

Katsomme vain HTTP-pyynnön ensimmäistä riviä, joten sen sijaan, että lukisimme koko pyynnön vektoriin, kutsumme next-metodia saadaksemme ensimmäisen kohteen iteraattorista. Ensimmäinen unwrap hoitaa Option-arvon ja lopettaa ohjelman, jos iteraattorilla ei ole kohteita. Toinen unwrap käsittelee Result-arvon ja sillä on sama vaikutus kuin listauksessa 21-2 lisätyllä map-kutsun unwrap-metodilla.

Seuraavaksi tarkistamme request_line-muuttujan nähdäksemme, vastaako se GET-pyyntöä polkuun /. Jos vastaa, if-lohko palauttaa HTML-tiedostomme sisällön.

Jos request_line ei vastaa GET-pyyntöä polkuun /, olemme saaneet jonkin muun pyynnön. Lisäämme hetken kuluttua koodia else-lohkoon vastataksemme kaikkiin muihin pyyntöihin.

Aja tämä koodi nyt ja pyydä 127.0.0.1:7878; sinun pitäisi saada HTML hello.html-tiedostosta. Jos teet minkä tahansa muun pyynnön, kuten 127.0.0.1:7878/something-else, saat yhteysvirheen, kuten listauksissa 21-1 ja 21-2 ajettaessa.

Lisätään nyt listauksen 21-7 koodi else-lohkoon palauttaaksemme vastauksen tilakoodilla 404, joka ilmaisee, että pyynnön sisältöä ei löytynyt. Palautamme myös HTML:ää sivulle, joka renderöidään selaimessa ilmaisten vastauksen loppukäyttäjälle.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-07/src/main.rs:here}}
}
Listing 21-7: Vastaaminen tilakoodilla 404 ja virhesivulla, jos mitään muuta kuin / pyydettiin

Tässä vastauksessamme on tilarivi tilakoodilla 404 ja syy-lausekkeella NOT FOUND. Vastauksen runko on HTML 404.html-tiedostossa. Sinun täytyy luoda 404.html-tiedosto hello.html-tiedoston viereen virhesivua varten; voit vapaasti käyttää mitä tahansa HTML:ää tai käyttää esimerkki-HTML:ää listauksessa 21-8.

Filename: 404.html
{{#include ../listings/ch21-web-server/listing-21-07/404.html}}
Listing 21-8: Esimerkkisisältö sivulle, joka lähetetään takaisin 404-vastauksen mukana

Näillä muutoksilla käynnistä palvelimesi uudelleen. Pyyntö 127.0.0.1:7878 pitäisi palauttaa hello.html-tiedoston sisällön, ja mikä tahansa muu pyyntö, kuten 127.0.0.1:7878/foo, pitäisi palauttaa virhe-HTML 404.html-tiedostosta.

Pieni refaktorointi

Tällä hetkellä if- ja else-lohkoissa on paljon toistoa: molemmat lukevat tiedostoja ja kirjoittavat tiedostojen sisällön streamiin. Ainoat erot ovat tilarivissä ja tiedostonimessä. Tehdään koodista ytimekkäämpää erottamalla nämä erot erillisiin if- ja else-riveihin, jotka määrittävät tilarivin ja tiedostonimen arvot muuttujiin; voimme sitten käyttää näitä muuttujia ehdottomasti koodissa tiedoston lukemiseen ja vastauksen kirjoittamiseen. Listausta 21-9 näyttää tuloksena olevan koodin suurten if- ja else-lohkojen korvaamisen jälkeen.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-09/src/main.rs:here}}
}
Listing 21-9: if- ja else-lohkojen refaktorointi sisältämään vain koodin, joka eroaa kahden tapauksen välillä

Nyt if- ja else-lohkot palauttavat vain sopivat arvot tilariville ja tiedostonimelle monikossa; käytämme sitten destrukturointia määrittääksemme nämä kaksi arvoa status_line- ja filename-muuttujiin käyttämällä kuviota let-lausekkeessa, kuten luvussa 19 käsiteltiin.

Aiemmin toistuva koodi on nyt if- ja else-lohkojen ulkopuolella ja käyttää status_line- ja filename-muuttujia. Tämä tekee kahden tapauksen erosta helpommin nähtävän, ja se tarkoittaa, että meillä on vain yksi paikka päivittää koodia, jos haluamme muuttaa tiedoston lukemisen ja vastauksen kirjoittamisen toimintaa. Listauksen 21-9 koodin käyttäytyminen on sama kuin listauksen 21-7.

Hienoa! Meillä on nyt yksinkertainen verkkopalvelin noin 40 rivillä Rust-koodia, joka vastaa yhteen pyyntöön sisältösivulla ja kaikkiin muihin pyyntöihin 404-vastauksella.

Tällä hetkellä palvelimemme toimii yhdessä säikeessä, mikä tarkoittaa, että se voi palvella vain yhtä pyyntöä kerrallaan. Tarkastellaan, miten tämä voi olla ongelma simuloimalla hitaita pyyntöjä. Korjaamme sitten sen, jotta palvelimemme voi käsitellä useita pyyntöjä samanaikaisesti.

Yksisäikeisen palvelimen muuttaminen monisäikeiseksi palvelimeksi

Yksisäikeisestä monisäikeiseksi palvelimeksi

Tällä hetkellä palvelin käsittelee jokaisen pyynnön vuorotellen, eli se ei käsittele toista yhteyttä ennen kuin ensimmäinen on käsitelty loppuun. Jos palvelin saisi yhä enemmän pyyntöjä, tämä peräkkäinen suoritus olisi yhä vähemmän optimaalinen. Jos palvelin saa pyynnön, jonka käsittely kestää kauan, myöhempien pyyntöjen täytyy odottaa, kunnes pitkä pyyntö on valmis — vaikka uudet pyynnöt voisi käsitellä nopeasti. Meidän täytyy korjata tämä, mutta ensin katsomme ongelmaa käytännössä.

Hitaan pyynnön simulointi nykyisessä palvelintoteutuksessa

Katsomme, miten hitaasti käsiteltävä pyyntö voi vaikuttaa muihin pyyntöihin, joita tehdään nykyiseen palvelintoteutukseemme. Listausta 21-10 toteuttaa pyynnön käsittelyn polulle /sleep simuloidulla hitaalla vastauksella, joka saa palvelimen nukkumaan viisi sekuntia ennen vastaamista.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-10/src/main.rs:here}}
}
Listing 21-10: Hitaan pyynnön simulointi nukkumalla 5 sekuntia

Vaihdoimme if-lausekkeesta match-lausekkeeseen, koska meillä on nyt kolme tapausta. Meidän täytyy eksplisiittisesti matchata request_line-viipaletta vasten merkkijonoliteraaliarvoja; match ei tee automaattista viittaamista ja käänteisviittaamista kuten yhtäsuuruusmetodi.

Ensimmäinen haara on sama kuin if-lohko listauksessa 21-9. Toinen haara matchaa pyynnön polulle /sleep. Kun tällainen pyyntö vastaanotetaan, palvelin nukkuu viisi sekuntia ennen onnistuneen HTML-sivun renderöintiä. Kolmas haara on sama kuin else-lohko listauksessa 21-9.

Näet, kuinka primitiivinen palvelimemme on: oikeat kirjastot käsittelisivät useiden pyyntöjen tunnistamisen paljon vähemmällä sanamäärällä!

Käynnistä palvelin komennolla cargo run. Avaa sitten kaksi selainikkunaa: toinen osoitteeseen http://127.0.0.1:7878/ ja toinen osoitteeseen http://127.0.0.1:7878/sleep. Jos syötät /-URI:n muutaman kerran kuten aiemmin, näet sen vastaavan nopeasti. Mutta jos syötät /sleep ja lataat sitten /, näet että / odottaa, kunnes sleep on nukkunut koko viisi sekuntiaan ennen latautumista.

On useita tekniikoita, joilla voisimme välttää pyyntöjen jonoutumisen hitaan pyynnön taakse, mukaan lukien asyncin käyttö kuten teimme luvussa 17; toteutamme säikeiden poolin.

Suorituskyvyn parantaminen säikeiden poolilla

Säikeiden pooli on joukko luotuja säikeitä, jotka odottavat ja ovat valmiita käsittelemään tehtävän. Kun ohjelma vastaanottaa uuden tehtävän, se osoittaa yhden poolin säikeistä tehtävään, ja kyseinen säie käsittelee tehtävän. Poolin jäljellä olevat säikeet ovat käytettävissä käsittelemään muita saapuvia tehtäviä, kun ensimmäinen säie käsittelee. Kun ensimmäinen säie on käsitellyt tehtävänsä, se palautetaan joutilaan säikeiden pooliin, valmiina käsittelemään uuden tehtävän. Säikeiden pooli mahdollistaa yhteyksien käsittelyn rinnakkain ja kasvattaa palvelimen suorituskykyä.

Rajoitamme säikeiden määrän poolissa pieneksi suojautuaksemme DoS-hyökkäyksiltä; jos ohjelma loisi uuden säikeen jokaiselle saapuvalle pyynnölle, joku, joka tekisi 10 miljoonaa pyyntöä palvelimellemme, voisi aiheuttaa kaaosta käyttämällä kaikki palvelimen resurssit ja pysäyttämällä pyyntöjen käsittelyn.

Sen sijaan, että loisimme rajattomasti säikeitä, pidämme kiinteän määrän säikeitä odottamassa poolissa. Saapuvat pyynnöt lähetetään pooliin käsiteltäväksi. Pooli ylläpitää jonoa saapuvista pyynnöistä. Jokainen poolin säikeistä poimii pyynnön tästä jonosta, käsittelee pyynnön ja pyytää sitten jonosta seuraavan pyynnön. Tällä suunnittelulla voimme käsitellä jopa N pyyntöä rinnakkain, missä N on säikeiden määrä. Jos jokainen säie vastaa pitkään kestävään pyyntöön, myöhemmät pyynnöt voivat silti jonoutua, mutta olemme kasvattaneet pitkään kestävien pyyntöjen määrää, jonka voimme käsitellä ennen kuin saavutamme tuon pisteen.

Tämä tekniikka on vain yksi monista tavoista parantaa verkkopalvelimen suorituskykyä. Muita vaihtoehtoja, joita voit tutkia, ovat fork/join-malli, yksisäikeinen async I/O -malli ja monisäikeinen async I/O -malli. Jos aihe kiinnostaa, voit lukea lisää muista ratkaisuista ja yrittää toteuttaa niitä; matalan tason kielellä kuten Rust kaikki nämä vaihtoehdot ovat mahdollisia.

Ennen kuin alamme toteuttaa säikeiden poolia, puhutaan siitä, miltä poolin käyttö näyttää. Kun suunnittelet koodia, asiakasrajapinnan kirjoittaminen ensin voi ohjata suunnitteluasi. Kirjoita koodin API niin, että se on rakenteeltaan sellainen, jolla haluat kutsua sitä; toteuta sitten toiminnallisuus tuon rakenteen sisällä sen sijaan, että toteuttaisit toiminnallisuuden ja suunnittelisit julkisen API:n jälkikäteen.

Samoin kuin käytimme testivetoista kehitystä luvun 12 projektissa, käytämme tässä kääntäjävetoista kehitystä. Kirjoitamme koodin, joka kutsuu haluamiamme funktioita, ja katsomme sitten kääntäjän virheitä selvittääksemme, mitä meidän pitää muuttaa seuraavaksi, jotta koodi toimii. Ennen sitä kuitenkin tutkimme tekniikan, jota emme aio käyttää lähtökohtana.

Säikeen luominen jokaiselle pyynnölle

Tutkitaan ensin, miltä koodimme näyttäisi, jos se loisi uuden säikeen jokaiselle yhteydelle. Kuten aiemmin mainittiin, tämä ei ole lopullinen suunnitelmamme mahdollisesti rajattoman määrän säikeiden luomisen ongelmien vuoksi, mutta se on lähtökohta toimivan monisäikeisen palvelimen saamiseksi ensin. Lisäämme sitten säikeiden poolin parannuksena, ja kahden ratkaisun vertailu on helpompaa. Listausta 21-11 näyttää muutokset main-funktioon luodaksemme uuden säikeen käsittelemään jokaista streamia for-silmukan sisällä.

Filename: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-11/src/main.rs:here}}
}
Listing 21-11: Uuden säikeen luominen jokaiselle streamille

Kuten opit luvussa 16, thread::spawn luo uuden säikeen ja suorittaa sitten sulkeuman koodin uudessa säikeessä. Jos suoritat tämän koodin ja lataat /sleep selaimessasi ja sitten / kahdessa muussa selainvälilehdessä, näet todellakin, etteivät /-pyynnöt joudu odottamaan /sleep-pyynnön valmistumista. Kuten mainitsimme, tämä kuitenkin lopulta ylikuormittaa järjestelmän, koska loisit uusia säikeitä ilman mitään rajaa.

Saatat myös muistaa luvusta 17, että tämä on juuri sellainen tilanne, jossa async ja await todella loistavat! Pidä tämä mielessä, kun rakennamme säikeiden poolia, ja mieti, miltä asiat näyttäisivät erilaisilta tai samankaltaisilta asyncin kanssa.

Rajallisen määrän säikeitä luovan rajapinnan luominen

Haluamme säikeiden poolimme toimivan samankaltaisella, tutulla tavalla, jotta vaihtaminen säikeistä säikeiden pooliin ei vaadi suuria muutoksia API:tamme käyttävään koodiin. Listausta 21-12 näyttää hypoteettisen rajapinnan ThreadPool-rakenteelle, jota haluamme käyttää thread::spawn-funktion sijaan.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-12/src/main.rs:here}}
Listing 21-12: Ihanteellinen ThreadPool-rajapintamme

Käytämme ThreadPool::new-funktiota luodaksemme uuden säikeiden poolin konfiguroitavalla määrällä säikeitä — tässä tapauksessa neljä. Sitten for-silmukassa pool.execute-funktiolla on samankaltainen rajapinta kuin thread::spawn-funktiolla: se ottaa sulkeuman, jonka poolin pitäisi suorittaa jokaiselle streamille. Meidän täytyy toteuttaa pool.execute niin, että se ottaa sulkeuman ja antaa sen poolin säikeelle suoritettavaksi. Tämä koodi ei vielä käänny, mutta yritämme, jotta kääntäjä voi ohjata meitä korjaamaan sen.

ThreadPool-rakenteen rakentaminen kääntäjävetoisella kehityksellä

Tee listauksen 21-12 muutokset tiedostoon src/main.rs, ja käytetään sitten cargo check-komennon kääntäjävirheitä kehityksen ohjaamiseen. Tässä on ensimmäinen virhe, jonka saamme:

{{#include ../listings/ch21-web-server/listing-21-12/output.txt}}

Hienoa! Tämä virhe kertoo, että tarvitsemme ThreadPool-tyypin tai -moduulin, joten rakennamme sellaisen nyt. ThreadPool-toteutuksemme on riippumaton siitä, millaista työtä verkkopalvelimemme tekee. Vaihdetaan siis hello-crate binaaricratesta kirjastocrateksi säilyttääksemme ThreadPool-toteutuksemme. Kun vaihdamme kirjastocrateksi, voimme myös käyttää erillistä säikeiden poolin kirjastoa mihin tahansa työhön, jossa haluamme käyttää säikeiden poolia — ei vain verkkopyyntöjen palvelemiseen.

Luo tiedosto src/lib.rs, joka sisältää seuraavan — yksinkertaisimman ThreadPool-rakenteen määrittelyn, jonka voimme toistaiseksi tehdä:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/no-listing-01-define-threadpool-struct/src/lib.rs}}

Muokkaa sitten tiedostoa main.rs tuomalla ThreadPool näkyviin kirjastocratesta lisäämällä seuraava koodi tiedoston src/main.rs alkuun:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch21-web-server/no-listing-01-define-threadpool-struct/src/main.rs:here}}

Tämä koodi ei vieläkään toimi, mutta tarkistetaan se uudelleen saadaksemme seuraavan korjattavan virheen:

{{#include ../listings/ch21-web-server/no-listing-01-define-threadpool-struct/output.txt}}

Tämä virhe osoittaa, että seuraavaksi meidän täytyy luoda ThreadPool-rakenteelle assosioitu funktio nimeltä new. Tiedämme myös, että new-funktiolla täytyy olla yksi parametri, joka voi ottaa 4 argumentiksi, ja sen pitäisi palauttaa ThreadPool-instanssi. Toteutetaan yksinkertaisin new-funktio, jolla on nämä ominaisuudet:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/no-listing-02-impl-threadpool-new/src/lib.rs}}

Valitsimme usize-tyypin size-parametrille, koska tiedämme, ettei negatiivinen säikeiden määrä ole järkevä. Tiedämme myös käyttäväämme tätä 4:ää säikeiden kokoelman elementtien määränä, mihin usize-tyyppi on tarkoitettu, kuten käsiteltiin ”Kokonaislukutyypit” -osiossa luvussa 3.

Tarkistetaan koodi uudelleen:

{{#include ../listings/ch21-web-server/no-listing-02-impl-threadpool-new/output.txt}}

Nyt virhe johtuu siitä, ettei ThreadPool-rakenteella ole execute-metodia. Muista ”Rajallisen määrän säikeitä luovan rajapinnan luominen” -osiosta, että päätimme säikeiden poolimme rajapinnan olevan samankaltainen kuin thread::spawn. Lisäksi toteutamme execute-funktion niin, että se ottaa annetun sulkeuman ja antaa sen poolin joutilaalle säikeelle suoritettavaksi.

Määrittelemme execute-metodin ThreadPool-rakenteelle ottamaan sulkeuman parametrina. Muista ”Kaapattujen arvojen siirtäminen sulkeumista” -osio luvusta 13, jossa voimme ottaa sulkeumia parametreina kolmella eri traitilla: Fn, FnMut ja FnOnce. Meidän täytyy päättää, millaista sulkeumaa käytämme tässä. Tiedämme päätyvämme tekemään jotain samankaltaista kuin standardikirjaston thread::spawn-toteutus, joten voimme katsoa, mitä rajoja thread::spawn-funktion parametri on. Dokumentaatio näyttää seuraavan:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

F-tyyppiparametri on se, josta olemme kiinnostuneita tässä; T-tyyppiparametri liittyy palautusarvoon, eikä se meitä kiinnosta. Näemme, että spawn käyttää FnOnce-traitia rajana F:lle. Tämä on todennäköisesti se, mitä haluamme, koska lopulta välitämme execute-funktioon saamamme argumentin spawn-funktiolle. Voimme olla varmempia siitä, että FnOnce on haluamamme trait, koska pyynnön käsittelevä säie suorittaa kyseisen pyynnön sulkeuman vain kerran — mikä vastaa FnOnce-traitin Once-osaa.

F-tyyppiparametrilla on myös trait-raja Send ja elinikäraja 'static, jotka ovat hyödyllisiä tilanteessamme: tarvitsemme Send-rajan siirtääksemme sulkeuman säikeestä toiseen ja 'static-rajan, koska emme tiedä, kuinka kauan säikeen suoritus kestää. Luodaan execute-metodi ThreadPool-rakenteelle, joka ottaa geneerisen parametrin tyypiltä F näillä rajoilla:

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/no-listing-03-define-execute/src/lib.rs:here}}

Käytämme edelleen ()-merkintää FnOnce-traitin jälkeen, koska tämä FnOnce edustaa sulkeumaa, joka ei ota parametreja ja palauttaa yksikkötyypin (). Kuten funktiomäärittelyissä, palautustyypin voi jättää pois allekirjoituksesta, mutta vaikka meillä ei olisi parametreja, tarvitsemme silti sulkeumat.

Taaskin tämä on yksinkertaisin execute-metodin toteutus: se ei tee mitään, mutta yritämme vain saada koodin kääntymään. Tarkistetaan se uudelleen:

{{#include ../listings/ch21-web-server/no-listing-03-define-execute/output.txt}}

Se kääntyy! Mutta huomaa, että jos yrität cargo run-komentoa ja teet pyynnön selaimessa, näet selaimessa samat virheet, joita näimme luvun alussa. Kirjastomme ei vielä kutsu execute-funktiolle välitettyä sulkeumaa!

Huom: Kielistä, joilla on tiukat kääntäjät kuten Haskell ja Rust, saatat kuulla sanonnan: ”jos koodi kääntyy, se toimii.” Mutta tämä sanonta ei ole yleispätevästi totta. Projektimme kääntyy, mutta se ei tee yhtään mitään! Jos rakentaisimme oikean, valmiin projektin, tämä olisi hyvä hetki alkaa kirjoittaa yksikkötestejä varmistaaksemme, että koodi kääntyy ja käyttäytyy haluamallamme tavalla.

Mieti: mitä olisi erilaista tässä, jos ajaisimme futuren sulkeuman sijaan?

Säikeiden määrän validointi new-funktiossa

Emme tee mitään new- ja execute-funktioiden parametreilla. Toteutetaan näiden funktioiden rungot haluamallamme käyttäytymisellä. Aloitetaan new-funktiosta. Aiemmin valitsimme etumerkittömän tyypin size-parametrille, koska pooli negatiivisella säikeiden määrällä ei ole järkevä. Pooli nollalla säikeellä ei kuitenkaan myöskään ole järkevä, vaikka nolla on täysin kelvollinen usize-arvo. Lisäämme koodia, joka tarkistaa, että size on suurempi kuin nolla, ennen kuin palautamme ThreadPool-instanssin, ja saamme ohjelman panikoimaan, jos se saa nollan assert!-makron avulla, kuten listauksessa 21-13.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-13/src/lib.rs:here}}
Listing 21-13: ThreadPool::new-toteutus, joka panikoi jos size on nolla

Olemme myös lisänneet dokumentaatiota ThreadPool-rakenteellemme doc-kommenteilla. Huomaa, että noudatimme hyviä dokumentointikäytäntöjä lisäämällä osion, joka kuvaa tilanteet, joissa funktiomme voi panikoida, kuten käsiteltiin luvussa 14. Kokeile ajaa cargo doc --open ja napsauta ThreadPool-rakennetta nähdäksesi, miltä new-funktion generoitu dokumentaatio näyttää!

Sen sijaan, että lisäisimme assert!-makron kuten teimme tässä, voisimme muuttaa new-funktion build-funktioksi ja palauttaa Result-arvon kuten teimme Config::build-funktiolla I/O-projektissa listauksessa 12-9. Päätimme kuitenkin tässä tapauksessa, että säikeiden poolin luominen ilman säikeitä on palautumaton virhe. Jos olet kunnianhimoinen, yritä kirjoittaa funktio nimeltä build seuraavalla allekirjoituksella vertailua varten new-funktion kanssa:

pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {

Tilaa säikeiden tallentamiseen

Nyt kun meillä on tapa varmistaa, että meillä on kelvollinen määrä säikeitä tallennettavaksi pooliin, voimme luoda nämä säikeet ja tallentaa ne ThreadPool-rakenteeseen ennen rakenteen palauttamista. Mutta miten ”tallennetaan” säie? Katsotaan thread::spawn-funktion allekirjoitusta uudelleen:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

spawn-funktio palauttaa JoinHandle<T>-arvon, missä T on sulkeuman palauttama tyyppi. Kokeillaan käyttää myös JoinHandle-tyyppiä ja katsotaan, mitä tapahtuu. Tapauksessamme sulkeumat, jotka välitämme säikeiden poolille, käsittelevät yhteyden eivätkä palauta mitään, joten T on yksikkötyyppi ().

Listauksen 21-14 koodi kääntyy, mutta ei vielä luo säikeitä. Olemme muuttaneet ThreadPool-rakenteen määrittelyn sisältämään vektorin thread::JoinHandle<()>-instansseja, alustaneet vektorin kapasiteetilla size, asettaneet for-silmukan, joka suorittaa koodia säikeiden luomiseksi, ja palauttaneet ThreadPool-instanssin, joka sisältää ne.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-14/src/lib.rs:here}}
Listing 21-14: Vektorin luominen ThreadPool-rakenteelle säikeiden säilyttämiseksi

Olemme tuoneet std::thread-moduulin näkyviin kirjastocratessa, koska käytämme thread::JoinHandle-tyyppiä ThreadPool-rakenteen vektorin alkioille.

Kun kelvollinen koko on vastaanotettu, ThreadPool-rakenteemme luo uuden vektorin, joka voi sisältää size alkiota. with_capacity-funktio tekee saman tehtävän kuin Vec::new, mutta tärkeällä erolla: se varaa tilan vektorille etukäteen. Koska tiedämme tarvitsevamme tallentaa size elementtiä vektoriin, tämä varaus etukäteen on hieman tehokkaampaa kuin Vec::new-funktion käyttö, joka muuttaa kokoaan elementtien lisäämisen yhteydessä.

Kun ajat cargo check-komennon uudelleen, sen pitäisi onnistua.

Worker-rakenne, joka vastaa koodin lähettämisestä ThreadPool-rakenteesta säikeelle

Jätimme kommentin for-silmukkaan listauksessa 21-14 säikeiden luomiseen liittyen. Tässä katsomme, miten säikeet todella luodaan. Standardikirjasto tarjoaa thread::spawn-funktion säikeiden luomiseen, ja thread::spawn odottaa saavansa koodin, jonka säikeen pitäisi suorittaa heti säikeen luomisen jälkeen. Tapauksessamme haluamme kuitenkin luoda säikeet ja saada ne odottamaan koodia, jonka lähetämme myöhemmin. Standardikirjaston säikeiden toteutus ei sisällä tapaa tehdä tätä; meidän täytyy toteuttaa se manuaalisesti.

Toteutamme tämän käyttäytymisen esittelemällä uuden tietorakenteen ThreadPool-rakenteen ja säikeiden väliin hallitsemaan tätä uutta käyttäytymistä. Kutsumme tätä tietorakennetta Worker-rakenteeksi, mikä on yleinen termi poolitoteutuksissa. Worker poimii suoritettavaa koodia ja suorittaa koodin Worker-rakenteen säikeessä.

Ajattele ravintolan keittiössä työskenteleviä ihmisiä: työntekijät odottavat, kunnes asiakkailta tulee tilauksia, ja he ovat vastuussa näiden tilausten ottamisesta ja täyttämisestä.

Sen sijaan, että tallentaisimme vektorin JoinHandle<()>-instansseja säikeiden pooliin, tallennamme Worker-rakenteen instansseja. Jokainen Worker tallentaa yhden JoinHandle<()>-instanssin. Toteutamme sitten metodin Worker-rakenteelle, joka ottaa suoritettavan sulkeuman ja lähettää sen jo käynnissä olevalle säikeelle suoritettavaksi. Annamme myös jokaiselle Worker-rakenteelle id-tunnisteen, jotta voimme erottaa poolin eri Worker-instanssit lokituksessa tai virheenkorjauksessa.

Tässä on uusi prosessi, joka tapahtuu, kun luomme ThreadPool-rakenteen. Toteutamme koodin, joka lähettää sulkeuman säikeelle, kun olemme asettaneet Worker-rakenteen tällä tavalla:

  1. Määrittele Worker-rakenne, joka sisältää id-kentän ja JoinHandle<()>-kentän.
  2. Muuta ThreadPool sisältämään vektori Worker-instansseja.
  3. Määrittele Worker::new-funktio, joka ottaa id-numeron ja palauttaa Worker-instanssin, joka sisältää id-kentän ja säikeen, joka on luotu tyhjällä sulkeumalla.
  4. ThreadPool::new-funktiossa käytä for-silmukan laskuria generoimaan id, luo uusi Worker kyseisellä id:llä ja tallenna worker vektoriin.

Jos haluat haastetta, yritä toteuttaa nämä muutokset itse ennen kuin katsot listauksen 21-15 koodia.

Valmis? Tässä on listausta 21-15 yhdellä tavalla tehdä edellä kuvatut muutokset.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-15/src/lib.rs:here}}
Listing 21-15: ThreadPool-rakenteen muuttaminen sisältämään Worker-instansseja säikeiden suoran tallentamisen sijaan

Olemme muuttaneet ThreadPool-rakenteen kentän nimen threads-kentästä workers-kentäksi, koska se sisältää nyt Worker-instansseja JoinHandle<()>-instanssien sijaan. Käytämme for-silmukan laskuria argumenttina Worker::new-funktiolle ja tallennamme jokaisen uuden Worker-rakenteen vektoriin nimeltä workers.

Ulkoinen koodi (kuten palvelimemme tiedostossa src/main.rs) ei tarvitse tietää toteutuksen yksityiskohtia Worker-rakenteen käytöstä ThreadPool-rakenteen sisällä, joten teemme Worker-rakenteen ja sen new-funktion yksityisiksi. Worker::new-funktio käyttää antamaamme id:tä ja tallentaa JoinHandle<()>-instanssin, joka on luotu luomalla uusi säie tyhjällä sulkeumalla.

Huom: Jos käyttöjärjestelmä ei voi luoda säiettä, koska järjestelmäresursseja ei ole tarpeeksi, thread::spawn panikoi. Se saa koko palvelimemme panikoimaan, vaikka osa säikeiden luomisesta saattaisi onnistua. Yksinkertaisuuden vuoksi tämä käyttäytyminen on ok, mutta tuotantokäyttöön tarkoitetussa säikeiden poolin toteutuksessa käyttäisit todennäköisesti std::thread::Builder -rakenteen ja sen spawn -metodia, joka palauttaa Result-arvon.

Tämä koodi kääntyy ja tallentaa määrän Worker-instansseja, jonka annoimme argumenttina ThreadPool::new-funktiolle. Mutta emme vieläkään käsittele sulkeumaa, jonka saamme execute-funktiossa. Katsotaan seuraavaksi, miten se tehdään.

Pyyntöjen lähettäminen säikeille kanavien kautta

Seuraava ongelma, johon tartumme, on se, että thread::spawn-funktiolle annetut sulkeumat eivät tee yhtään mitään. Tällä hetkellä saamme sulkeuman, jonka haluamme suorittaa, execute-metodissa. Mutta meidän täytyy antaa thread::spawn-funktiolle sulkeuma suoritettavaksi, kun luomme jokaisen Worker-rakenteen ThreadPool-rakenteen luomisen yhteydessä.

Haluamme juuri luomiemme Worker-rakenteiden hakevan suoritettavan koodin jonosta, jonka ThreadPool pitää hallussaan, ja lähettävän kyseisen koodin säikeelleen suoritettavaksi.

Luvussa 16 opitut kanavat — yksinkertainen tapa kommunikoida kahden säikeen välillä — sopisivat tähän käyttötapaukseen täydellisesti. Käytämme kanavaa työjonona, ja execute lähettää työn ThreadPool-rakenteesta Worker-instansseille, jotka lähettävät työn säikeelleen. Tässä on suunnitelma:

  1. ThreadPool luo kanavan ja pitää lähettäjän hallussaan.
  2. Jokainen Worker pitää vastaanottajan hallussaan.
  3. Luomme uuden Job-rakenteen, joka sisältää sulkeumat, jotka haluamme lähettää kanavaa pitkin.
  4. execute-metodi lähettää suoritettavan työn lähettäjän kautta.
  5. Säikeessään Worker silmukoi vastaanottajansa yli ja suorittaa kaikkien vastaanottamiensa töiden sulkeumat.

Aloitetaan luomalla kanava ThreadPool::new-funktiossa ja tallentamalla lähettäjä ThreadPool-instanssiin, kuten listauksessa 21-16. Job-rakenne ei toistaiseksi sisällä mitään, mutta se on tyyppi, jota lähetämme kanavaa pitkin.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-16/src/lib.rs:here}}
Listing 21-16: ThreadPool-rakenteen muuttaminen tallentamaan kanavan lähettäjä, joka välittää Job-instansseja

ThreadPool::new-funktiossa luomme uuden kanavan ja annamme poolin pitää lähettäjän hallussaan. Tämä kääntyy onnistuneesti.

Kokeillaan välittää kanavan vastaanottaja jokaiselle Worker-rakenteelle, kun säikeiden pooli luo kanavan. Tiedämme haluavamme käyttää vastaanottajaa säikeessä, jonka Worker-instanssit luovat, joten viittaamme receiver-parametriin sulkeumassa. Listauksen 21-17 koodi ei vielä aivan käänny.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-17/src/lib.rs:here}}
Listing 21-17: Vastaanottajan välittäminen jokaiselle Worker-rakenteelle

Olemme tehneet pieniä ja suoraviivaisia muutoksia: välitämme vastaanottajan Worker::new-funktiolle ja käytämme sitä sitten sulkeuman sisällä.

Kun yritämme tarkistaa tämän koodin, saamme tämän virheen:

{{#include ../listings/ch21-web-server/listing-21-17/output.txt}}

Koodi yrittää välittää receiver-arvon useille Worker-instansseille. Tämä ei toimi, kuten muistat luvusta 16: Rustin tarjoama kanavatoteutus on useita tuottajia, yksi kuluttaja. Tämä tarkoittaa, ettei voi vain kloonata kanavan kuluttajapäätä korjataksemme tämän koodin. Emme myöskään halua lähettää viestiä useita kertoja useille kuluttajille; haluamme yhden viestilistan useilla Worker-instansseilla siten, että jokainen viesti käsitellään kerran.

Lisäksi työn poimiminen kanavan jonosta edellyttää receiver-arvon mutatoimista, joten säikeiden täytyy jakaa ja muokata receiver-arvoa turvallisesti; muuten saatamme saada kilpailutilanteita (kuten käsiteltiin luvussa 16).

Muista luvussa 16 käsitellyt säieturvalliset älykkäät osoittimet: jakaaksemme omistajuuden useiden säikeiden välillä ja salliaksemme säikeiden mutatoida arvoa, meidän täytyy käyttää Arc<Mutex<T>>-rakennetta. Arc-tyyppi antaa useiden Worker-instanssien omistaa vastaanottajan, ja Mutex varmistaa, että vain yksi Worker saa työn vastaanottajalta kerrallaan. Listausta 21-18 näyttää tarvittavat muutokset.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-18/src/lib.rs:here}}
Listing 21-18: Vastaanottajan jakaminen Worker-instanssien kesken Arc- ja Mutex-rakenteiden avulla

ThreadPool::new-funktiossa laitamme vastaanottajan Arc- ja Mutex-rakenteisiin. Jokaiselle uudelle Worker-rakenteelle kloonaamme Arc-rakenteen kasvattaaksemme viittauslaskuria, jotta Worker-instanssit voivat jakaa vastaanottajan omistajuuden.

Näillä muutoksilla koodi kääntyy! Olemme lähellä maalia!

execute-metodin toteuttaminen

Toteutetaan vihdoin execute-metodi ThreadPool-rakenteelle. Muutamme myös Job-rakenteen rakenteesta tyyppialiaaksi trait-objektille, joka sisältää execute-funktion vastaanottaman sulkeuman tyypin. Kuten käsiteltiin ”Tyyppialiaasit ja tyyppien synonyymit” -osiossa luvussa 20, tyyppialiaasit antavat meidän lyhentää pitkiä tyyppejä helpompaa käyttöä varten. Katso listaus 21-19.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-19/src/lib.rs:here}}
Listing 21-19: Job-tyyppialiaasin luominen Box-rakenteelle, joka sisältää jokaisen sulkeuman, ja työn lähettäminen kanavaa pitkin

Kun olemme luoneet uuden Job-instanssin execute-funktiossa saamallamme sulkeumalla, lähetämme työn kanavan lähettäjäpäähän. Kutsumme unwrap-metodia send-funktiolle tapauksessa, jossa lähetys epäonnistuu. Tämä voi tapahtua esimerkiksi, jos pysäytämme kaikki säikeemme suorittamasta, mikä tarkoittaa, että vastaanottajapää on lopettanut uusien viestien vastaanottamisen. Tällä hetkellä emme voi pysäyttää säikeitämme suorittamasta: säikeemme jatkavat suoritusta niin kauan kuin pooli on olemassa. Käytämme unwrap-metodia, koska tiedämme epäonnistumistapauksen olevan mahdoton, mutta kääntäjä ei tiedä sitä.

Mutta emme ole vielä valmiita! Worker-rakenteessa sulkeuma, joka välitetään thread::spawn-funktiolle, viittaa edelleen vain kanavan vastaanottajapäähän. Sen sijaan tarvitsemme sulkeuman, joka silmukoi ikuisesti pyytäen työtä kanavan vastaanottajapäästä ja suorittaen työn saadessaan sellaisen. Tehdään listauksessa 21-20 näytetty muutos Worker::new-funktioon.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-20/src/lib.rs:here}}
Listing 21-20: Töiden vastaanottaminen ja suorittaminen Worker-instanssin säikeessä

Tässä kutsumme ensin lock-metodia receiver-arvolla hankkiaksemme mutexin, ja sitten kutsumme unwrap-metodia panikoidaksemme mahdollisista virheistä. Lukon hankkiminen voi epäonnistua, jos mutex on myrkytetty tilassa, mikä voi tapahtua, jos jokin toinen säie panikoi pitäessään lukkoa sen sijaan, että vapauttaisi sen. Tässä tilanteessa unwrap-kutsu, joka saa tämän säikeen panikoimaan, on oikea toimenpide. Voit vapaasti muuttaa tämän unwrap-kutsun expect-kutsuksi merkityksellisellä virheilmoituksella.

Jos saamme lukon mutexiin, kutsumme recv-metodia vastaanottaaksemme Job-työn kanavalta. Lopullinen unwrap-kutsu ohittaa myös mahdolliset virheet tässä, mikä voi tapahtua, jos lähettäjää pitävä säie on sulkeutunut — samalla tavalla kuin send-metodi palauttaa Err-arvon, jos vastaanottaja sulkeutuu.

recv-kutsu blokkaa, joten jos työtä ei vielä ole, nykyinen säie odottaa, kunnes työ tulee saataville. Mutex<T> varmistaa, että vain yksi Worker-säie kerrallaan yrittää pyytää työtä.

Säikeiden poolimme on nyt toimivassa tilassa! Aja cargo run ja tee muutama pyyntö:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
 --> src/lib.rs:7:5
  |
6 | pub struct ThreadPool {
  |            ---------- field in this struct
7 |     workers: Vec<Worker>,
  |     ^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: fields `id` and `thread` are never read
  --> src/lib.rs:48:5
   |
47 | struct Worker {
   |        ------ fields in this struct
48 |     id: usize,
   |     ^^
49 |     thread: thread::JoinHandle<()>,
   |     ^^^^^^

warning: `hello` (lib) generated 2 warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.

Onnistui! Meillä on nyt säikeiden pooli, joka käsittelee yhteyksiä asynkronisesti. Enintään neljä säiettä luodaan, joten järjestelmämme ei ylikuormitu, jos palvelin saa paljon pyyntöjä. Jos teemme pyynnön polulle /sleep, palvelin voi palvella muita pyyntöjä antamalla toisen säikeen suorittaa ne.

Huom: Jos avaat /sleep-polun useissa selainikkunoissa samanaikaisesti, ne saattavat latautua viiden sekunnin välein yksi kerrallaan. Jotkin verkkoselaimet suorittavat useita saman pyynnön instansseja peräkkäin välimuistisyistä. Tämä rajoitus ei johdu verkkopalvelimestamme.

Tämä on hyvä hetki pysähtyä ja miettiä, miten listauksien 21-18, 21-19 ja 21-20 koodi olisi erilainen, jos käyttäisimme futureja sulkeuman sijaan tehtävänä. Mitkä tyypit muuttuisivat? Miten metodien allekirjoitukset olisivat erilaisia, jos ollenkaan? Mitkä koodin osat pysyisivät samoina?

Kun olet oppinut while let -silmukasta luvuissa 17 ja 19, saatat ihmetellä, miksi emme kirjoittaneet Worker-säikeen koodia kuten listauksessa 21-21.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-21/src/lib.rs:here}}
Listing 21-21: Vaihtoehtoinen Worker::new-toteutus while let -silmukan avulla

Tämä koodi kääntyy ja suoritetaan, mutta ei tuota haluttua säikeiden käyttäytymistä: hidas pyyntö saa edelleen muut pyynnöt odottamaan käsittelyä. Syy on hieman hienovarainen: Mutex-rakenteella ei ole julkista unlock-metodia, koska lukon omistajuus perustuu lock-metodin palauttaman LockResult<MutexGuard<T>>-rakenteen sisällä olevan MutexGuard<T>-rakenteen elinikään. Käännösaikana lainantarkistin voi sitten pakottaa säännön, että Mutex-rakenteen suojaamaa resurssia ei voi käyttää, ellemme pidä lukkoa. Tämä toteutus voi kuitenkin myös johtaa siihen, että lukkoa pidetään pidempään kuin tarkoitettu, jos emme ole tietoisia MutexGuard<T>-rakenteen elinikästä.

Listauksen 21-20 koodi, joka käyttää let job = receiver.lock().unwrap().recv().unwrap();, toimii, koska let-lausekkeella kaikki lausekkeen oikean puolen väliaikaiset arvot pudotetaan heti, kun let-lause päättyy. while let (ja if let ja match) eivät kuitenkaan pudota väliaikaisia arvoja ennen kuin liittyvä lohko päättyy. Listauksessa 21-21 lukko pysyy hallussa job()-kutsun keston ajan, mikä tarkoittaa, etteivät muut Worker-instanssit voi vastaanottaa töitä.

Siisti sammutus ja siivous

Hallittu sammutus ja siivous

Listauksen 21-20 koodi vastaa pyyntöihin asynkronisesti säikeiden poolin avulla, kuten aioimme. Saamme varoituksia workers-, id- ja thread-kentistä, joita emme käytä suoraan — ne muistuttavat meitä siitä, ettei siivousta tehdä. Kun pysäytämme pääsäikeen vähemmän tyylikkäällä ctrl-C-menetelmällä, kaikki muut säikeet pysähtyvät heti, vaikka ne olisivat keskellä pyynnön käsittelyä.

Seuraavaksi toteutamme Drop-traitin kutsumaan join-metodia jokaiselle poolin säikeelle, jotta ne voivat viimeistellä työnsä ennen sulkemista. Toteutamme myös tavan kertoa säikeille, etteivät ne enää hyväksy uusia pyyntöjä ja että ne sammuttavat toimintansa. Nähdäksemme koodin toiminnassa muokkaamme palvelinta hyväksymään vain kaksi pyyntöä ennen säikeiden poolin hallittua sammuttamista.

Yksi huomio matkan varrella: mikään tästä ei vaikuta sulkeumien suorittamiseen liittyvään koodiin, joten kaikki olisi sama, jos käyttäisimme säikeiden poolia async-runtimeen.

Drop-traitin toteuttaminen ThreadPool-rakenteelle

Aloitetaan toteuttamalla Drop säikeiden poolille. Kun pooli pudotetaan, kaikkien säikeiden pitäisi liittyä (join) varmistaakseen, että ne viimeistelevät työnsä. Listausta 21-22 näyttää ensimmäisen yrityksen Drop-toteutuksesta; tämä koodi ei vielä aivan toimi.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-22/src/lib.rs:here}}
Listing 21-22: Jokaisen säikeen liittäminen, kun säikeiden pooli menee näkyvyysalueen ulkopuolelle

Ensin silmukoidaan säikeiden poolin jokaisen Worker-instanssin yli. Käytämme &mut, koska self on muuttuva viite ja meidän täytyy myös pystyä mutatoimaan worker-instanssia. Jokaiselle worker-instanssille tulostamme viestin, joka kertoo kyseisen Worker-instanssin sammuttuvan, ja kutsumme sitten join-metodia kyseisen Worker-instanssin säikeelle. Jos join-kutsu epäonnistuu, käytämme unwrap-metodia saadaksemme Rustin panikoimaan ja siirtymään hallitsemattomaan sammutukseen.

Tässä on virhe, jonka saamme kääntäessämme tämän koodin:

{{#include ../listings/ch21-web-server/listing-21-22/output.txt}}

Virhe kertoo, ettei join-metodia voi kutsua, koska meillä on vain muuttuva lainaus jokaisesta worker-instanssista ja join ottaa argumenttinsa omistukseen. Ratkaistaksemme ongelman meidän täytyy siirtää säie Worker-instanssista, joka omistaa thread-kentän, jotta join voi kuluttaa säikeen. Yksi tapa tehdä tämä on sama lähestymistapa kuin listauksessa 18-15. Jos Worker sisältäisi Option<thread::JoinHandle<()>>-kentän, voisimme kutsua take-metodia Option-rakenteella siirtääksemme arvon Some-variantista ja jättääksemme tilalle None-variantin. Toisin sanoen käynnissä oleva Worker sisältäisi Some-variantin thread-kentässä, ja kun haluaisimme siivota Worker-instanssin, korvaisimme Some-variantin None-variantilla, jolloin Worker-instanssilla ei olisi enää säiettä suoritettavaksi.

Tämä tilanne kuitenkin ilmenisi ainoastaan Worker-instanssin pudotuksen yhteydessä. Vastineeksi meidän pitäisi käsitellä Option<thread::JoinHandle<()>>-tyyppiä aina, kun käytämme worker.thread-kenttää. Idiomatista Rustia käyttää Option-tyyppiä melko paljon, mutta kun huomaat kääriväsi jotain, jonka tiedät aina olevan olemassa, Option-tyyppiin kiertotienä, on hyvä etsiä vaihtoehtoisia lähestymistapoja koodin selkeyttämiseksi ja virhealttiuden vähentämiseksi.

Tässä tapauksessa parempi vaihtoehto on olemassa: Vec::drain-metodi. Se ottaa alueparametrin määrittääkseen, mitkä alkiot poistetaan vektorista, ja palauttaa iteraattorin näistä alkioista. ..-alue syntaksin välittäminen poistaa vektorista jokaisen arvon.

Meidän täytyy siis päivittää ThreadPool-rakenteen drop-toteutus näin:

Filename: src/lib.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/no-listing-04-update-drop-definition/src/lib.rs:here}}
}

Tämä ratkaisee kääntäjävirheen eikä vaadi muita muutoksia koodiimme. Huomaa, että koska drop voidaan kutsua panikoinnin aikana, unwrap voi myös panikoida ja aiheuttaa kaksinkertaisen paniikin, joka kaataa ohjelman heti ja keskeyttää käynnissä olevan siivouksen. Tämä on ok esimerkkiohjelmalle, mutta sitä ei suositella tuotantokoodissa.

Signaalin lähettäminen säikeille lopettaakseen työjonon kuuntelun

Kaikkien tekemiemme muutosten jälkeen koodimme kääntyy ilman varoituksia. Huono uutinen on kuitenkin, ettei koodi vielä toimi haluamallamme tavalla. Avain on Worker-instanssien säikeiden suorittamien sulkeumien logiikassa: tällä hetkellä kutsumme join-metodia, mutta se ei sammuta säikeitä, koska ne silmukoivat ikuisesti etsien töitä. Jos yritämme pudottaa ThreadPool-rakenteen nykyisellä drop-toteutuksellamme, pääsäie jumittuu ikuisesti odottamaan ensimmäisen säikeen valmistumista.

Korjataksemme ongelman tarvitsemme muutoksen ThreadPool-rakenteen drop-toteutukseen ja sitten muutoksen Worker-silmukkaan.

Ensin muutamme ThreadPool-rakenteen drop-toteutusta pudottamaan sender-kentän eksplisiittisesti ennen säikeiden valmistumisen odottamista. Listausta 21-23 näyttää muutokset ThreadPool-rakenteeseen sender-kentän eksplisiittiseen pudottamiseen. Toisin kuin säikeen kohdalla, tarvitsemme tässä Option-tyypin, jotta voimme siirtää sender-kentän pois ThreadPool-rakenteesta Option::take-metodilla.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-23/src/lib.rs:here}}
Listing 21-23: sender-kentän eksplisiittinen pudottaminen ennen Worker-säikeiden liittämistä

sender-kentän pudottaminen sulkee kanavan, mikä ilmaisee, ettei enempää viestejä lähetetä. Kun näin tapahtuu, kaikki recv-kutsut, joita Worker-instanssit tekevät äärettömässä silmukassaan, palauttavat virheen. Listauksessa 21-24 muutamme Worker-silmukan poistumaan siististi silmukasta tässä tapauksessa, mikä tarkoittaa, että säikeet valmistuvat, kun ThreadPool-rakenteen drop-toteutus kutsuu niille join-metodia.

Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-24/src/lib.rs:here}}
Listing 21-24: Silmukasta eksplisiittinen poistuminen, kun recv palauttaa virheen

Nähdäksemme koodin toiminnassa muokataan main-funktiota hyväksymään vain kaksi pyyntöä ennen palvelimen hallittua sammuttamista, kuten listauksessa 21-25.

Filename: src/main.rs
{{#rustdoc_include ../listings/ch21-web-server/listing-21-25/src/main.rs:here}}
Listing 21-25: Palvelimen sammuttaminen kahden pyynnön käsittelyn jälkeen poistumalla silmukasta

Et haluaisi oikean maailman verkkopalvelimen sammuttavan toimintansa vain kahden pyynnön jälkeen. Tämä koodi vain demonstroi, että hallittu sammutus ja siivous toimivat.

take-metodi on määritelty Iterator-traitissa ja rajoittaa iteraation enintään kahteen ensimmäiseen alkioon. ThreadPool menee näkyvyysalueen ulkopuolelle main-funktion lopussa, ja drop-toteutus suoritetaan.

Käynnistä palvelin komennolla cargo run ja tee kolme pyyntöä. Kolmannen pyynnön pitäisi epäonnistua, ja terminaalissasi pitäisi näkyä suunnilleen seuraavanlainen tuloste:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

Saatat nähdä eri järjestyksessä Worker-tunnisteita ja viestejä. Näemme viesteistä, miten koodi toimii: Worker-instanssit 0 ja 3 saivat kaksi ensimmäistä pyyntöä. Palvelin lopetti yhteyksien hyväksymisen toisen yhteyden jälkeen, ja ThreadPool-rakenteen Drop-toteutus alkaa suorittua ennen kuin Worker 3 edes aloittaa työnsä. sender-kentän pudottaminen katkaisee yhteyden kaikkiin Worker-instansseihin ja käskee niitä sammuttamaan toimintansa. Worker-instanssit tulostavat kukin viestin katkaistessaan yhteyden, ja sitten säikeiden pooli kutsuu join-metodia odottaakseen jokaisen Worker-säikeen valmistumista.

Huomaa yksi mielenkiintoinen piirre tässä suorituksessa: ThreadPool pudotti sender-kentän, ja ennen kuin yksikään Worker sai virheen recv-kutsusta, yritimme liittää (join) Worker 0:aa. Worker 0 ei ollut vielä saanut virhettä recv-kutsusta, joten pääsäie blokkaantui odottamaan Worker 0:n valmistumista. Sillä välin Worker 3 vastaanotti työn, ja sitten kaikki säikeet saivat virheen. Kun Worker 0 valmistui, pääsäie odotti loput Worker-instanssit valmiiksi. Silloin ne olivat kaikki poistuneet silmukoistaan ja pysähtyneet.

Onnittelut! Olemme nyt viimeistelleet projektimme; meillä on perusverkkopalvelin, joka käyttää säikeiden poolia vastatakseen asynkronisesti. Pystymme suorittamaan palvelimen hallitun sammutuksen, joka siivoaa kaikki poolin säikeet.

Tässä on koko koodi viitteeksi:

Filename: src/main.rs
{{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/main.rs}}
Filename: src/lib.rs
{{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/lib.rs}}

Voimme tehdä täällä vielä enemmän! Jos haluat jatkaa projektin parantamista, tässä muutamia ideoita:

  • Lisää dokumentaatiota ThreadPool-rakenteelle ja sen julkisille metodeille.
  • Lisää testejä kirjaston toiminnallisuudelle.
  • Muuta unwrap-kutsut vankemmaksi virheenkäsittelyksi.
  • Käytä ThreadPool-rakennetta johonkin muuhun tehtävään kuin verkkopyyntöjen palvelemiseen.
  • Etsi säikeiden pooli -crate crates.io-sivustolta ja toteuta samankaltainen verkkopalvelin cratea käyttäen. Vertaa sitten sen API:a ja vankkuutta toteuttamaamme säikeiden pooliin.

Yhteenveto

Hyvin tehty! Olet päässyt kirjan loppuun! Haluamme kiittää sinua siitä, että liityit mukaan tälle Rust-kierrokselle. Olet nyt valmis toteuttamaan omia Rust-projektejasi ja auttamaan muiden projekteissa. Muista, että Rust-yhteisössä on tervetulleita rustilaisia, jotka auttavat mielellään kaikissa haasteissa Rust-matkallasi.

Liite

Seuraavat osiot sisältävät viitemateriaalia, josta voi olla hyötyä Rust-opintojesi aikana.

A - Avainsanat

Liite A: Avainsanat

Seuraavissa luetteloissa on avainsanoja, jotka Rust-kieli on varannut nykyiseen tai tulevaan käyttöön. Sellaisenaan niitä ei voi käyttää tunnisteina (paitsi raakatunnisteina, kuten käsitellään kohdassa ”Raakatunnisteet”). Tunnisteet ovat funktioiden, muuttujien, parametrien, rakenteiden kenttien, moduulien, pakettien, vakioiden, makrojen, staattisten arvojen, attribuuttien, tyyppien, traitien tai elinikien nimiä.

Tällä hetkellä käytössä olevat avainsanat

Seuraavassa on luettelo tällä hetkellä käytössä olevista avainsanoista ja niiden toiminnallisuudesta.

  • as: Suorittaa primitiivisen tyypinmuunnoksen, poistaa moniselitteisyyden sen traitin osalta, joka sisältää kohteen, tai uudelleennimeää kohteita use-lauseissa.
  • async: Palauttaa Future-olion estämättä nykyistä säiettä.
  • await: Keskeyttää suorituksen, kunnes Future-olion tulos on valmis.
  • break: Poistuu silmukasta välittömästi.
  • const: Määrittää vakioita tai vakioraakaosoittimia.
  • continue: Jatkaa seuraavaan silmukkaiteraatioon.
  • crate: Moduulipolussa viittaa paketin juureen.
  • dyn: Dynaaminen lähetys trait-oliolle.
  • else: Varavaihtoehto if- ja if let -ohjausrakenteille.
  • enum: Määrittää luettelon.
  • extern: Linkittää ulkoisen funktion tai muuttujan.
  • false: Totuusarvon epätosi-literaali.
  • fn: Määrittää funktion tai funktio-osoitintyypin.
  • for: Käy läpi iteraattorin kohteita, toteuttaa traitin tai määrittää korkeamman asteen eliniän.
  • if: Haarautuu ehdollisen lausekkeen tuloksen perusteella.
  • impl: Toteuttaa sisäänrakennettua tai trait-toiminnallisuutta.
  • in: Osa for-silmukan syntaksia.
  • let: Sitoo muuttujan.
  • loop: Toistaa silmukkaa ehdottomasti.
  • match: Vertaa arvoa kuvioihin.
  • mod: Määrittää moduulin.
  • move: Saattaa sulkeuman ottamaan omistukseen kaikista kaappauksistaan.
  • mut: Ilmaisee muuttuvuutta viittauksissa, raakaosoittimissa tai kuviosidonnissa.
  • pub: Ilmaisee julkisen näkyvyyden rakenteiden kentissä, impl-lohkoissa tai moduuleissa.
  • ref: Sitoo viittauksena.
  • return: Palaa funktiosta.
  • Self: Tyyppialias tyypille, jota määritellään tai toteutetaan.
  • self: Metodin kohde tai nykyinen moduuli.
  • static: Globaali muuttuja tai koko ohjelman suorituksen kestävä elinikä.
  • struct: Määrittää rakenteen.
  • super: Nykyisen moduulin ylämoduuli.
  • trait: Määrittää traitin.
  • true: Totuusarvon tosi-literaali.
  • type: Määrittää tyyppialiasin tai assosioituneen tyypin.
  • union: Määrittää unionin; on avainsana vain unioni-määrittelyssä.
  • unsafe: Merkitsee turvatonta koodia, funktiota, traitia tai toteutusta.
  • use: Tuo symboleja näkyvyysalueelle.
  • where: Ilmaisee tyyppiä rajoittavia lausekkeita.
  • while: Toistaa silmukkaa ehdollisesti lausekkeen tuloksen perusteella.

Tulevaisuutta varten varatut avainsanat

Seuraavilla avainsanoilla ei ole vielä toiminnallisuutta, mutta Rust on varannut ne mahdollista tulevaa käyttöä varten:

  • abstract
  • become
  • box
  • do
  • final
  • gen
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

Raakatunnisteet

Raakatunnisteet ovat syntaksia, jonka avulla avainsanoja voi käyttää paikoissa, joissa niitä ei normaalisti sallittaisi. Raakatunniste muodostetaan lisäämällä avainsanan eteen r#.

Esimerkiksi match on avainsana. Jos yrität kääntää seuraavan funktion, joka käyttää match-sanaa nimenään:

Filename: src/main.rs

fn match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

saat tämän virheen:

error: expected identifier, found keyword `match`
 --> src/main.rs:4:4
  |
4 | fn match(needle: &str, haystack: &str) -> bool {
  |    ^^^^^ expected identifier, found keyword

Virhe kertoo, että avainsanaa match ei voi käyttää funktion tunnisteena. Käyttääksesi match-sanaa funktion nimenä, sinun täytyy käyttää raakatunnistesyntaksia näin:

Filename: src/main.rs

fn r#match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

fn main() {
    assert!(r#match("foo", "foobar"));
}

Tämä koodi kääntyy ilman virheitä. Huomaa r#-etuliite funktion nimessä sekä sen määrittelyssä että siinä kohdassa, jossa funktiota kutsutaan main-funktiossa.

Raakatunnisteet mahdollistavat minkä tahansa sanan käytön tunnisteena, vaikka sana olisi varattu avainsana. Tämä antaa enemmän vapautta valita tunnistenimiä ja helpottaa integraatiota ohjelmiin, jotka on kirjoitettu kielellä, jossa nämä sanat eivät ole avainsanoja. Lisäksi raakatunnisteet mahdollistavat kirjastojen käytön, jotka on kirjoitettu eri Rust-editionilla kuin mitä pakettisi käyttää. Esimerkiksi try ei ole avainsana editionissa 2015, mutta on editioneissa 2018, 2021 ja 2024. Jos riippuvuutesi on kirjoitettu editionilla 2015 ja siinä on try-funktio, sinun täytyy käyttää raakatunnistesyntaksia, tässä tapauksessa r#try, kutsuessasi kyseistä funktiota koodistasi myöhemmissä editioneissa. Katso lisätietoja editioneista liitteestä E.

B - Operaattorit ja symbolit

Liite B: Operaattorit ja symbolit

Tämä liite sisältää Rustin syntaksin sanaston, mukaan lukien operaattorit ja muut symbolit, jotka esiintyvät yksinään tai polkujen, geneeristen tyyppien, trait-rajojen, makrojen, attribuuttien, kommenttien, tuplejen ja hakasulkeiden yhteydessä.

Operaattorit

Taulukko B-1 sisältää Rustin operaattorit, esimerkin siitä, miltä operaattori näyttäisi kontekstissa, lyhyen selityksen ja tiedon siitä, onko operaattori ylikuormitettavissa. Jos operaattori on ylikuormitettavissa, luettelossa on myös siihen liittyvä trait, jota käytetään operaattorin ylikuormittamiseen.

Taulukko B-1: Operaattorit

OperatorExampleExplanationOverloadable?
!ident!(...), ident!{...}, ident![...]Makron laajennus
!!exprBittinen tai looginen komplementtiNot
!=expr != exprEriarvoisuusvertailuPartialEq
%expr % exprJakojäännösRem
%=var %= exprJakojäännös ja sijoitusRemAssign
&&expr, &mut exprLainaaminen
&&type, &mut type, &'a type, &'a mut typeLainatun osoittimen tyyppi
&expr & exprBittinen JABitAnd
&=var &= exprBittinen JA ja sijoitusBitAndAssign
&&expr && exprOikosulkeva looginen JA
*expr * exprKertolaskuMul
*=var *= exprKertolasku ja sijoitusMulAssign
**exprDereferenssiDeref
**const type, *mut typeRaaka osoitin
+trait + trait, 'a + traitYhdistetty tyyppirajoite
+expr + exprYhteenlaskuAdd
+=var += exprYhteenlasku ja sijoitusAddAssign
,expr, exprArgumenttien ja elementtien erotin
-- exprAritmeettinen negaatioNeg
-expr - exprVähennyslaskuSub
-=var -= exprVähennyslasku ja sijoitusSubAssign
->fn(...) -> type, |…| -> typeFunktion ja sulkeuman paluuarvon tyyppi
.expr.identKentän käyttö
.expr.ident(expr, ...)Metodikutsu
.expr.0, expr.1, and so onTuplen indeksointi
...., expr.., ..expr, expr..exprOikealta avoin väli-literaaliPartialOrd
..=..=expr, expr..=exprOikealta suljettu väli-literaaliPartialOrd
....exprRakenneliteraalin päivityssyntaksi
..variant(x, ..), struct_type { x, .. }”Ja loput” -kuviosidonta
...expr...expr(Vanhentunut, käytä ..= sijaan) Kuviossa: suljettu välikuvio
/expr / exprJakolaskuDiv
/=var /= exprJakolasku ja sijoitusDivAssign
:pat: type, ident: typeRajoitteet
:ident: exprRakennekentän alustus
:'a: loop {...}Silmukan label
;expr;Lauseen ja kohteen pääte
;[...; len]Osa kiinteäkokoista taulukkosyntaksia
<<expr << exprSiirto vasemmalleShl
<<=var <<= exprSiirto vasemmalle ja sijoitusShlAssign
<expr < exprPienempi kuin -vertailuPartialOrd
<=expr <= exprPienempi tai yhtä suuri kuin -vertailuPartialOrd
=var = expr, ident = typeSijoitus/ekvivalenssi
==expr == exprYhtäsuuruusvertailuPartialEq
=>pat => exprOsa match-haaran syntaksia
>expr > exprSuurempi kuin -vertailuPartialOrd
>=expr >= exprSuurempi tai yhtä suuri kuin -vertailuPartialOrd
>>expr >> exprSiirto oikealleShr
>>=var >>= exprSiirto oikealle ja sijoitusShrAssign
@ident @ patKuviosidonta
^expr ^ exprBittinen eksklusiivinen TAIBitXor
^=var ^= exprBittinen eksklusiivinen TAI ja sijoitusBitXorAssign
|pat | patKuviovaihtoehdot
|expr | exprBittinen TAIBitOr
|=var |= exprBittinen TAI ja sijoitusBitOrAssign
||expr || exprOikosulkeva looginen TAI
?expr?Virheen propagointi

Muut kuin operaattorisymbolit

Seuraavissa taulukoissa on kaikki symbolit, jotka eivät toimi operaattoreina; toisin sanoen ne eivät käyttäydy kuten funktio- tai metodikutsu.

Taulukko B-2 näyttää symbolit, jotka esiintyvät yksinään ja ovat kelvollisia monissa eri paikoissa.

Taulukko B-2: Itsenäinen syntaksi

SymbolExplanation
'identNimetty elinikä tai silmukan label
Digits immediately followed by u8, i32, f64, usize, and so onTietyn tyypin numeerinen literaali
"..."Merkkijonoliteraali
r"...", r#"..."#, r##"..."##, and so onRaaka merkkijonoliteraali; escape-merkkejä ei käsitellä
b"..."Tavumerkkijonoliteraali; muodostaa tavutaulukon merkkijonon sijaan
br"...", br#"..."#, br##"..."##, and so onRaaka tavumerkkijonoliteraali; raa’an ja tavumerkkijonoliteraalin yhdistelmä
'...'Merkkiliteraali
b'...'ASCII-tavuliteraali
|…| exprSulkeuma
!Aina tyhjä pohjatyyppi divergoiville funktioille
_”Ohitettu” kuviosidonta; käytetään myös kokonaislukuliteraalien luettavuuteen

Taulukko B-3 näyttää symbolit, jotka esiintyvät polun yhteydessä moduulihierarkian kautta kohteeseen.

Taulukko B-3: Polkuun liittyvä syntaksi

SymbolExplanation
ident::identNimiavaruuspolku
::pathPolku suhteessa paketin juureen (eli eksplisiittisesti absoluuttinen polku)
self::pathPolku suhteessa nykyiseen moduuliin (eli eksplisiittisesti suhteellinen polku)
super::pathPolku suhteessa nykyisen moduulin ylämoduuliin
type::ident, <type as trait>::identAssosioituneet vakiot, funktiot ja tyypit
<type>::...Assosioitu kohde tyypille, jota ei voi nimetä suoraan (esimerkiksi <&T>::..., <[T]>::..., and so on)
trait::method(...)Metodikutsun poistaminen moniselitteisyydestä nimeämällä sen määrittävä trait
type::method(...)Metodikutsun poistaminen moniselitteisyydestä nimeämällä tyyppi, jolle se on määritelty
<type as trait>::method(...)Metodikutsun poistaminen moniselitteisyydestä nimeämällä trait ja tyyppi

Taulukko B-4 näyttää symbolit, jotka esiintyvät geneeristen tyyppiparametrien käytön yhteydessä.

Taulukko B-4: Geneeriset tyypit

SymbolExplanation
path<...>Määrittää parametrit geneeriselle tyypille tyypissä (esimerkiksi Vec<u8>)
path::<...>, method::<...>Määrittää parametrit geneeriselle tyypille, funktiolle tai metodille lausekkeessa; usein kutsutaan turbofishiksi (esimerkiksi "42".parse::<i32>())
fn ident<...> ...Määrittele geneerinen funktio
struct ident<...> ...Määrittele geneerinen rakenne
enum ident<...> ...Määrittele geneerinen luettelo
impl<...> ...Määrittele geneerinen toteutus
for<...> typeKorkeamman asteen elinkaarirajat
type<ident=type>Geneerinen tyyppi, jossa yhdellä tai useammalla assosioituneella tyypillä on tietyt sijoitukset (esimerkiksi Iterator<Item=T>)

Taulukko B-5 näyttää symbolit, jotka esiintyvät geneeristen tyyppiparametrien rajoittamisen yhteydessä trait-rajoilla.

Taulukko B-5: Trait-rajoitteet

SymbolExplanation
T: UGeneerinen parametri T rajoitettu tyyppeihin, jotka toteuttavat U-traitin
T: 'aGeneerisen tyypin T täytyy elää eliniän 'a yli (eli tyyppi ei voi transitiivisesti sisältää viitteitä, joiden elinikä on lyhyempi kuin 'a)
T: 'staticGeneerinen tyyppi T ei sisällä lainattuja viitteitä muita kuin 'static-viitteitä
'b: 'aGeneerisen eliniän 'b täytyy elää eliniän 'a yli
T: ?SizedSalli geneerisen tyyppiparametrin olla dynaamisesti mitoitettu tyyppi
'a + trait, trait + traitYhdistetty tyyppirajoite

Taulukko B-6 näyttää symbolit, jotka esiintyvät makrojen kutsumisen tai määrittelyn sekä kohteelle määritettyjen attribuuttien yhteydessä.

Taulukko B-6: Makrot ja attribuutit

SymbolExplanation
#[meta]Ulompi attribuutti
#![meta]Sisempi attribuutti
$identMakron substituutio
$ident:kindMakron metamuuttuja
$(...)...Makron toisto
ident!(...), ident!{...}, ident![...]Makrokutsu

Taulukko B-7 näyttää symbolit, joilla luodaan kommentteja.

Taulukko B-7: Kommentit

SymbolExplanation
//Rivikommentti
//!Sisempi rividokumentaatiokommentti
///Ulompi rividokumentaatiokommentti
/*...*/Lohkokommentti
/*!...*/Sisempi lohkodokumentaatiokommentti
/**...*/Ulompi lohkodokumentaatiokommentti

Taulukko B-8 näyttää kontekstit, joissa kaarisulkeita käytetään.

Taulukko B-8: Kaarisulkeet

SymbolExplanation
()Tyhjä tuple (eli unit), sekä literaali että tyyppi
(expr)Sulkeistettu lauseke
(expr,)Yksielementtinen tuple-lauseke
(type,)Yksielementtinen tuple-tyyppi
(expr, ...)Tuple-lauseke
(type, ...)Tuple-tyyppi
expr(expr, ...)Funktiokutsulauseke; käytetään myös tuple-struct- ja tuple-enum-varianttien alustamiseen

Taulukko B-9 näyttää kontekstit, joissa aaltosulkeita käytetään.

Taulukko B-9: Aaltosulkeet

ContextExplanation
{...}Lohkolauseke
Type {...}Rakenneliteraali

Taulukko B-10 näyttää kontekstit, joissa hakasulkeita käytetään.

Taulukko B-10: Hakasulkeet

ContextExplanation
[...]Taulukkoliteraali
[expr; len]Taulukkoliteraali, joka sisältää len kappaletta expr-lauseketta
[type; len]Taulukkotyyppi, joka sisältää len kappaletta type-tyyppiä
expr[expr]Kokoelman indeksointi; ylikuormitettavissa (Index, IndexMut)
expr[..], expr[a..], expr[..b], expr[a..b]Kokoelman indeksointi, joka esittää kokoelman viipalointia käyttäen Range-, RangeFrom-, RangeTo- tai RangeFull-tyyppiä ”indeksinä”

C - Johdettavat traitit

Liite C: Johdettavat traitit

Kirjan eri kohdissa olemme käsitelleet derive-attribuuttia, jonka voi lisätä rakenne- tai luettelomäärittelyyn. derive-attribuutti luo koodia, joka toteuttaa traitin oletustoteutuksellaan tyypille, johon olet lisännyt derive-syntaksin.

Tässä liitteessä tarjoamme viitteen kaikista standardikirjaston traiteista, joita voi käyttää derive-attribuutin kanssa. Kukin osio käsittelee:

  • Mitä operaattoreita ja metodeja tämän traitin johdattaminen mahdollistaa
  • Mitä derive-attribuutin tarjoama toteutus tekee
  • Mitä traitin toteuttaminen merkitsee tyypille
  • Milloin traitin toteuttaminen on sallittua tai kiellettyä
  • Esimerkkejä toiminnoista, jotka vaativat traitin

Jos haluat eri käyttäytymisen kuin derive-attribuutti tarjoaa, katso standardikirjaston dokumentaatiosta kunkin traitin manuaalista toteutusta varten.

Tässä luetellut traitit ovat ainoat standardikirjaston määrittelemät traitit, jotka voi toteuttaa omille tyypeilleen derive-attribuutilla. Muilla standardikirjaston traiteilla ei ole järkevää oletuskäyttäytymistä, joten ne täytyy toteuttaa tavalla, joka sopii tavoitteeseesi.

Esimerkki traitista, jota ei voi johdattaa, on Display, joka käsittelee loppukäyttäjälle tarkoitettua muotoilua. Sinun tulisi aina harkita sopivaa tapaa näyttää tyyppi loppukäyttäjälle. Mitkä osat tyypistä loppukäyttäjän tulisi nähdä? Mitkä osat olisivat hänelle olennaisia? Mikä tietomuoto olisi hänelle merkityksellisin? Rust-kääntäjällä ei ole tätä näkemystä, joten se ei voi tarjota sinulle sopivaa oletuskäyttäytymistä.

Tässä liitteessä annettu luettelo johdettavista traiteista ei ole kattava: kirjastot voivat toteuttaa derive-attribuutin omille traiteilleen, jolloin derive-attribuutilla käytettävissä olevien traitien luettelo on aidosti avoin. derive-toteutus edellyttää proseduraalisen makron käyttöä, jota käsitellään luvun 20 kohdassa ”Mukautetut derive- makrot”.

Debug ohjelmoijan tulostusta varten

Debug-trait mahdollistaa virheenkorjausmuotoilun formaattijonoissa, jonka ilmaiset lisäämällä :? {}-paikkamerkkien sisään.

Debug-traitin avulla voit tulostaa tyypin instansseja virheenkorjausta varten, jotta sinä ja muut ohjelmoijat, jotka käyttävät tyyppiäsi, voitte tarkastella instanssia ohjelman suorituksen tietyssä kohdassa.

Debug-trait vaaditaan esimerkiksi assert_eq!-makron käytössä. Tämä makro tulostaa argumenteiksi annettujen instanssien arvot, jos yhtäsuuruusväite epäonnistuu, jotta ohjelmoijat näkevät, miksi kaksi instanssia eivät olleet yhtä suuria.

PartialEq ja Eq yhtäsuuruusvertailuihin

PartialEq-trait mahdollistaa tyypin instanssien vertailun yhtäsuuruuden tarkistamiseksi ja ==- sekä !=-operaattorien käytön.

PartialEq-traitin johdattaminen toteuttaa eq-metodin. Kun PartialEq johdetaan rakenteille, kaksi instanssia on yhtä suuri vain, jos kaikki kentät ovat yhtä suuria, ja instanssit eivät ole yhtä suuria, jos mikä tahansa kentistä ei ole yhtä suuri. Kun se johdetaan luetteloille, jokainen variantti on yhtä suuri itsensä kanssa eikä yhtä suuri muiden varianttien kanssa.

PartialEq-trait vaaditaan esimerkiksi assert_eq!-makron käytössä, joka tarvitsee mahdollisuuden vertailla kahden instanssin yhtäsuuruutta.

Eq-traitilla ei ole metodeja. Sen tarkoitus on ilmaista, että jokainen annotoidun tyypin arvo on yhtä suuri itsensä kanssa. Eq-traitia voi soveltaa vain tyyppeihin, jotka toteuttavat myös PartialEq-traitin, vaikka kaikki PartialEq-traitin toteuttavat tyypit eivät voi toteuttaa Eq-traitia. Yksi esimerkki tästä ovat liukulukutyypit: liukulukujen toteutus sanoo, että kaksi ei-lukua (NaN) -arvoa eivät ole yhtä suuria toistensa kanssa.

Esimerkki tilanteesta, jossa Eq vaaditaan, on avaimet HashMap<K, V>-rakenteessa, jotta HashMap<K, V> voi kertoa, ovatko kaksi avainta samat.

PartialOrd ja Ord järjestysvertailuihin

PartialOrd-trait mahdollistaa tyypin instanssien vertailun lajittelua varten. Tyyppiä, joka toteuttaa PartialOrd-traitin, voi käyttää <, >, <= ja >= -operaattoreiden kanssa. PartialOrd-traitia voi soveltaa vain tyyppeihin, jotka toteuttavat myös PartialEq-traitin.

PartialOrd-traitin johdattaminen toteuttaa partial_cmp-metodin, joka palauttaa Option<Ordering>-arvon, joka on None, kun annetut arvot eivät tuota järjestystä. Esimerkki arvosta, joka ei tuota järjestystä, vaikka useimmat kyseisen tyypin arvot ovat vertailtavissa, on liukuluvun NaN-arvo. partial_cmp-metodin kutsuminen millä tahansa liukuluvulla ja NaN-liukuluvulla palauttaa None.

Kun se johdetaan rakenteille, PartialOrd vertaa kahta instanssia vertaamalla kunkin kentän arvoa siinä järjestyksessä, jossa kentät esiintyvät rakennemäärittelyssä. Kun se johdetaan luetteloille, rakennemäärittelyssä aiemmin julistetut variantit katsotaan myöhemmin lueteltuja variantteja pienemmiksi.

PartialOrd-trait vaaditaan esimerkiksi rand-paketin gen_range-metodissa, joka tuottaa satunnaisen arvon välilausekkeen määrittämässä välissä.

Ord-traitin avulla tiedät, että annotoidun tyypin kahdelle mille tahansa arvolle on olemassa kelvollinen järjestys. Ord-trait toteuttaa cmp-metodin, joka palauttaa Ordering-arvon Option<Ordering>-arvon sijaan, koska kelvollinen järjestys on aina mahdollinen. Ord-traitia voi soveltaa vain tyyppeihin, jotka toteuttavat myös PartialOrd- ja Eq-traitit (ja Eq edellyttää PartialEq-traitia). Kun se johdetaan rakenteille ja luetteloille, cmp käyttäytyy samalla tavalla kuin partial_cmp PartialOrd-traitin johdetussa toteutuksessa.

Esimerkki tilanteesta, jossa Ord vaaditaan, on arvojen tallentaminen BTreeSet<T>- rakenteeseen, joka säilyttää tietoa arvojen lajittelujärjestyksen perusteella.

Clone ja Copy arvojen monistamiseen

Clone-trait mahdollistaa arvon eksplisiittisen syväkopion luomisen, ja monistusprosessi voi sisältää mielivaltaisen koodin suorittamisen ja keon tietojen kopioimisen. Katso lisätietoja Clone-traitista luvun 4 kohdasta ”Muuttujat ja tiedon vuorovaikutus Clone-traitin kanssa”.

Clone-traitin johdattaminen toteuttaa clone-metodin, joka koko tyypille toteutettuna kutsuu clone-metodia kunkin tyypin osan kohdalla. Tämä tarkoittaa, että kaikkien kenttien tai arvojen tyypissä täytyy myös toteuttaa Clone, jotta Clone voidaan johdattaa.

Esimerkki tilanteesta, jossa Clone vaaditaan, on to_vec-metodin kutsuminen viipaleella. Viipale ei omista sisältämiään tyypin instansseja, mutta to_vec-metodin palauttaman vektorin täytyy omistaa instanssinsa, joten to_vec kutsuu clone-metodia jokaisen kohteen kohdalla. Näin ollen viipaleessa säilytettävän tyypin täytyy toteuttaa Clone.

Copy-trait mahdollistaa arvon monistamisen kopioimalla vain pinossa säilytetyt bitit; mielivaltaista koodia ei tarvita. Katso lisätietoja Copy-traitista luvun 4 kohdasta ”Vain pinossa säilytettävä data: Copy.

Copy-trait ei määrittele metodeja, jotta ohjelmoijat eivät voisi ylikuormittaa niitä ja rikkoa oletusta, ettei mielivaltaista koodia suoriteta. Näin kaikki ohjelmoijat voivat olettaa, että arvon kopioiminen on hyvin nopeaa.

Voit johdattaa Copy-traitin mille tahansa tyypille, jonka kaikki osat toteuttavat Copy-traitin. Copy-traitin toteuttavan tyypin täytyy myös toteuttaa Clone, koska Copy-traitin toteuttavalla tyypillä on triviaali Clone-toteutus, joka tekee saman tehtävän kuin Copy.

Copy-traitia vaaditaan harvoin; Copy-traitin toteuttavilla tyypeillä on käytettävissä optimointeja, joten sinun ei tarvitse kutsua clone-metodia, mikä tekee koodista ytimekkäämpää.

Kaiken, mitä Copy-traitilla voi tehdä, voi myös saavuttaa Clone-traitilla, mutta koodi voi olla hitaampaa tai joutua käyttämään clone-metodia joissakin kohdissa.

Hash arvon kartoittamiseen kiinteäkokoiseen arvoon

Hash-trait mahdollistaa mielivaltaisen kokoisen tyypin instanssin kartoittamisen kiinteäkokoiseen arvoon tiivistefunktion avulla. Hash-traitin johdattaminen toteuttaa hash-metodin. Johdetussa hash-metodin toteutuksessa yhdistetään kunkin tyypin osan hash-metodin kutsumisen tulos, mikä tarkoittaa, että kaikkien kenttien tai arvojen täytyy myös toteuttaa Hash, jotta Hash voidaan johdattaa.

Esimerkki tilanteesta, jossa Hash vaaditaan, on avainten tallentaminen HashMap<K, V>- rakenteeseen tietojen tehokkaaseen säilyttämiseen.

Default oletusarvoille

Default-trait mahdollistaa tyypille oletusarvon luomisen. Default-traitin johdattaminen toteuttaa default-funktion. Johdetussa default-funktion toteutuksessa kutsutaan default-funktiota kunkin tyypin osan kohdalla, mikä tarkoittaa, että kaikkien kenttien tai arvojen tyypissä täytyy myös toteuttaa Default, jotta Default voidaan johdattaa.

Default::default-funktiota käytetään yleisesti yhdessä rakennepäivityssyntaksin kanssa, jota käsitellään luvun 5 kohdassa ”Instanssien luominen muista instansseista rakenne- päivityssyntaksilla”. Voit mukauttaa muutamia rakenteen kenttiä ja asettaa sitten oletusarvon lopuille kentille käyttämällä ..Default::default()-syntaksia.

Default-trait vaaditaan, kun käytät unwrap_or_default-metodia Option<T>-instansseilla. Jos Option<T> on None, unwrap_or_default-metodi palauttaa Default::default-funktion tuloksen tyypille T, joka on tallennettuna Option<T>-rakenteeseen.

D - Hyödylliset kehitystyökalut

Liite D: Hyödylliset kehitystyökalut

Tässä liitteessä käsittelemme joitakin hyödyllisiä kehitystyökaluja, joita Rust-projekti tarjoaa. Käymme läpi automaattisen muotoilun, nopeita tapoja soveltaa varoitusten korjauksia, linterin ja integraation IDE:iden kanssa.

Automaattinen muotoilu rustfmt-työkalulla

rustfmt-työkalu muotoilee koodisi yhteisön koodityylin mukaisesti. Monet yhteistyöhankkeet käyttävät rustfmt-työkalua estääkseen kiistoja siitä, mitä tyyliä Rust-kirjoituksessa käytetään: kaikki muotoilevat koodinsa työkalulla.

Rust-asennukset sisältävät rustfmt-työkalun oletuksena, joten järjestelmässäsi pitäisi jo olla ohjelmat rustfmt ja cargo-fmt. Nämä kaksi komentoa vastaavat rustc- ja cargo- komentoja siinä mielessä, että rustfmt tarjoaa tarkemman hallinnan ja cargo-fmt ymmärtää Cargoa käyttävän projektin käytännöt. Muotoillaksesi minkä tahansa Cargo-projektin, anna seuraava komento:

$ cargo fmt

Tämän komennon suorittaminen muotoilee kaiken nykyisen paketin Rust-koodin. Sen pitäisi muuttaa vain koodityyliä, ei koodin semantiikkaa. Lisätietoja rustfmt-työkalusta löytyy sen dokumentaatiosta.

Koodin korjaaminen rustfix-työkalulla

rustfix-työkalu sisältyy Rust-asennuksiin ja voi automaattisesti korjata kääntäjän varoituksia, joilla on selkeä korjaustapa, joka todennäköisesti on se, mitä haluat. Olet todennäköisesti nähnyt kääntäjän varoituksia aiemmin. Esimerkiksi tarkastele tätä koodia:

Filename: src/main.rs

fn main() {
    let mut x = 42;
    println!("{x}");
}

Tässä määrittelemme muuttujan x muuttuvaksi, mutta emme koskaan muuta sitä. Rust varoittaa siitä:

$ cargo build
   Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
 --> src/main.rs:2:9
  |
2 |     let mut x = 0;
  |         ----^
  |         |
  |         help: remove this `mut`
  |
  = note: `#[warn(unused_mut)]` on by default

Varoitus ehdottaa, että poistamme mut-avainsanan. Voimme soveltaa ehdotusta automaattisesti rustfix-työkalulla suorittamalla komennon cargo fix:

$ cargo fix
    Checking myprogram v0.1.0 (file:///projects/myprogram)
      Fixing src/main.rs (1 fix)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

Kun katsomme tiedostoa src/main.rs uudelleen, huomaamme, että cargo fix on muuttanut koodin:

Filename: src/main.rs

fn main() {
    let x = 42;
    println!("{x}");
}

Muuttuja x on nyt muuttumaton, eikä varoitusta enää näy.

Voit myös käyttää cargo fix -komentoa siirtääksesi koodiasi eri Rust-editionien välillä. Editioneista kerrotaan liitteessä E.

Lisää linttauksia Clippyllä

Clippy-työkalu on kokoelma linttauksia, jotka analysoivat koodiasi, jotta voit havaita yleisiä virheitä ja parantaa Rust-koodiasi. Clippy sisältyy vaki Rust-asennuksiin.

Suorittaaksesi Clippyn linttauksia missä tahansa Cargo-projektissa, anna seuraava komento:

$ cargo clippy

Esimerkiksi oletetaan, että kirjoitat ohjelman, joka käyttää matemaattisen vakion likiarvoa, kuten piitä, kuten tämä ohjelma tekee:

Filename: src/main.rs
fn main() {
    let x = 3.1415;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

Tämän projektin cargo clippy -suoritus tuottaa tämän virheen:

error: approximate value of `f{32, 64}::consts::PI` found
 --> src/main.rs:2:13
  |
2 |     let x = 3.1415;
  |             ^^^^^^
  |
  = note: `#[deny(clippy::approx_constant)]` on by default
  = help: consider using the constant directly
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant

Tämä virhe kertoo, että Rustissa on jo tarkempi PI-vakio määriteltynä ja että ohjelmasi olisi oikeampi, jos käyttäisit vakiota likiarvon sijaan. Muuttaisit sitten koodisi käyttämään PI-vakiota.

Seuraava koodi ei tuota Clippyltä virheitä tai varoituksia:

Filename: src/main.rs
fn main() {
    let x = std::f64::consts::PI;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

Lisätietoja Clippystä löytyy sen dokumentaatiosta.

IDE-integraatio rust-analyzer-työkalulla

IDE-integraation helpottamiseksi Rust-yhteisö suosittelee rust-analyzer -työkalun käyttöä. Tämä työkalu on joukko kääntäjäkeskeisiä apuohjelmia, jotka käyttävät Language Server Protocol -määritystä, joka on spesifikaatio IDE:iden ja ohjelmointikielten väliselle viestinnälle. Erilaiset asiakkaat voivat käyttää rust-analyzer-työkalua, kuten Visual Studio Coden Rust-analyzer-liitännäinen.

Vieraile rust-analyzer-projektin kotisivulla saadaksesi asennusohjeet ja asenna sitten kielipalvelintuki omaan IDE:esi. IDE:si saa ominaisuuksia, kuten automaattisen täydennyksen, siirtymisen määrittelyyn ja rivinsisäiset virheet.

E - Editions

Liite E - Rust-versiot (Editions)

Luvussa 1 näimme, että cargo new lisää hieman metadataa Cargo.toml-tiedostoon liittyen Rustin versioon (edition). Tämä liite selittää, mitä se tarkoittaa!

Rust-kieli ja sen kääntäjä julkaisevat uuden version kuuden viikon välein, mikä tarkoittaa jatkuvaa uusien ominaisuuksien virtaa. Toiset ohjelmointikielet julkaisevat suurempia muutoksia harvemmin; Rust päivittää kieltään pienemmissä erissä, mutta usein. Ajan mittaan nämä pienet muutokset kasaantuvat merkittäväksi kehitykseksi. Yksittäisten versioiden välillä voi olla vaikea huomata suuria eroja, mutta esimerkiksi Rust 1.10 ja Rust 1.31 -versioiden välillä ero voi olla huomattava.

Joka kahden tai kolmen vuoden välein Rust-tiimi julkaisee uuden Rust-version (edition). Jokainen uusi versio kokoaa yhteen kaikki edelliset muutokset selkeäksi kokonaisuudeksi, jossa dokumentaatio ja työkalut ovat ajan tasalla. Uudet versiot julkaistaan osana normaalia kuuden viikon julkaisusykliä.

Versioiden merkitys eri käyttäjäryhmille:

  • Aktiivisille Rustin käyttäjille uudet versiot kokoavat pienet muutokset helposti ymmärrettäväksi paketiksi.
  • Niille, jotka eivät vielä käytä Rustia, uusi versio voi viestiä merkittävistä parannuksista ja kannustaa tutustumaan Rustiin uudelleen.
  • Rustin kehittäjille uusi versio toimii projektin yhteisenä tavoitteena ja virstanpylväänä.

Tämän tekstin kirjoitushetkellä saatavilla on neljä Rust-versiota: Rust 2015, Rust 2018, Rust 2021 ja Rust 2024. Tämä kirja on kirjoitettu Rust 2024 -version mukaisesti.

edition-avain Cargo.toml-tiedostossa

Cargo.toml-tiedostossa oleva edition-avain määrittää, mitä Rust-versiota kääntäjän tulisi käyttää koodillesi. Jos tätä avainta ei ole, Rust käyttää 2015-versiota oletuksena taaksepäin yhteensopivuuden varmistamiseksi.

Jokainen projekti voi valita haluamansa Rust-version, eikä sen tarvitse pysyä oletusarvoisessa 2015-versiossa. Uudet versiot voivat sisältää yhteensopimattomia muutoksia, kuten uusien avainsanojen käyttöönoton, mikä voi vaikuttaa olemassa olevaan koodiin. Jos et kuitenkaan valitse uudempaa versiota, koodisi kääntyy edelleen, vaikka päivität Rustin kääntäjäversiota.

Kaikki Rustin kääntäjäversiot tukevat aiempia versioita, ja ne voivat yhdistää eri versioita käyttäviä crate-kirjastoja. Versiomuutokset vaikuttavat vain siihen, miten kääntäjä alun perin jäsentää koodin. Tämä tarkoittaa, että jos käytät Rust 2015 -versiota ja jokin riippuvuutesi käyttää Rust 2018 -versiota, projektisi kääntyy ja toimii normaalisti. Sama pätee toisinpäin: Rust 2018 -versiolla kääntyvä projekti voi käyttää Rust 2015 -versiolla käännettyjä riippuvuuksia.

Uuden version käyttöönotto

Selvennyksen vuoksi: suurin osa Rustin uusista ominaisuuksista toimii kaikissa versioissa. Kehittäjät hyötyvät uusista ominaisuuksista riippumatta siitä, mitä versiota he käyttävät. Joissain tapauksissa, erityisesti uusien avainsanojen käyttöönoton yhteydessä, jotkut ominaisuudet voivat olla saatavilla vain tietyissä Rust-versioissa. Jos haluat hyödyntää näitä ominaisuuksia, sinun täytyy vaihtaa projektisi käyttämään uudempaa Rust-versiota.

Lisätietoa Rustin eri versioista löytyy Edition Guide -oppaasta, joka kuvaa tarkasti eri versioiden erot ja selittää, miten voit päivittää koodisi uuteen versioon cargo fix -komennolla.

F - Translations of the Book

Liite F: Kirjan käännökset

Resursseja muilla kielillä kuin englanniksi. Suurin osa muista kielistä on vielä työn alla; katso Translations-merkintä, jos haluat auttaa tai ilmoittaa uudesta käännöksestä!

G - How Rust is Made and “Nightly Rust”

Liite G - Miten Rustia kehitetään ja mitä on “Nightly Rust”

Tässä liitteessä käsitellään Rustin kehitysprosessia ja sen vaikutuksia Rust-kehittäjiin.

Vakautta ilman pysähtyneisyyttä

Rust panostaa erityisen paljon koodin vakauteen. Tarkoituksena on, että Rust toimii luotettavana perustana, jonka varaan voi rakentaa. Jos kieli muuttuisi jatkuvasti, tämä olisi mahdotonta. Toisaalta, jos uusia ominaisuuksia ei voisi kokeilla ennen niiden julkaisua, mahdolliset ongelmat huomattaisiin vasta, kun muutoksia ei enää voida tehdä.

Ratkaisumme tähän ongelmaan on “vakautta ilman pysähtyneisyyttä”, ja ohjaavana periaatteena on tämä: Rustin päivittämisen ei tulisi koskaan aiheuttaa pelkoa. Jokaisen uuden version tulisi olla helppo ottaa käyttöön, tuoda uusia ominaisuuksia, vähentää bugeja ja nopeuttaa kääntämistä.

Julkaisumalli – “junamalli”

Rust kehitetään “junamallilla”, jossa kaikki kehitystyö tehdään master-haarassa. Julkaisut tapahtuvat ohjelmistojen julkaisujunamallin mukaisesti, jota ovat käyttäneet muun muassa Cisco IOS ja muut suuret ohjelmistoprojektit. Rustilla on kolme julkaisukanavaa:

  • Nightly (yöllinen koonti uusilla ominaisuuksilla)
  • Beta (testausvaihe ennen vakaata julkaisua)
  • Stable (vakaa, tuotantokäyttöön tarkoitettu versio)

Useimmat Rust-kehittäjät käyttävät ensisijaisesti vakaa (stable) -versiota, mutta kehittäjät, jotka haluavat kokeilla uusimpia kokeellisia ominaisuuksia, käyttävät nightly- tai beta-versioita.

Joka kuudes viikko, uusi beta-julkaisu haarautuu master-haarasta. Tämän jälkeen se testataan, ja vakaa julkaisu tehdään beta-haarasta kuuden viikon kuluttua.

nightly: * - - * - - * - - * - - * - - * - * - *
                     |                         |
beta:                * - - - - - - - - *       *
                                       |
stable:                                *

Koska Rustin julkaisut ovat aikataulutettuja, jokainen uusi ominaisuus, joka ei ehdi tiettyyn julkaisuun, tulee mukaan seuraavaan. Tämä vähentää painetta yrittää saada keskeneräisiä ominaisuuksia mukaan viime hetkellä.

Ylläpito ja tuen elinkaari

Rust-projekti tukee vain uusinta vakaata versiota. Kun uusi vakaa versio julkaistaan, vanha versio ei enää saa virhekorjauksia. Tämä tarkoittaa, että jokainen versio saa tukea vain kuusi viikkoa.

Epävakaat ominaisuudet (Unstable Features)

Rustin kehitysmallissa kokeelliset ominaisuudet lisätään ensin master-haaraan, mutta ne pidetään feature flag -valinnan takana. Tämä tarkoittaa, että:

  • Jos käytät nightly-versiota, voit ottaa kokeelliset ominaisuudet käyttöön feature flag -määritteellä.
  • Jos käytät beta- tai stable-versiota, kokeellisia ominaisuuksia ei voi käyttää.

Tämä mahdollistaa uusien ominaisuuksien testaamisen ja arvioinnin ennen niiden lopullista vakauttamista. Vakaa Rust tarjoaa rock-solid-kokemuksen ilman odottamattomia muutoksia.

Tämä kirja käsittelee vain vakaita ominaisuuksia, sillä kehitteillä olevat ominaisuudet voivat muuttua ennen kuin ne tulevat vakaisiin julkaisuihin. Kokeellisten ominaisuuksien dokumentaatio löytyy Rustin virallisilta sivuilta.

Rustup ja Nightly Rustin käyttö

rustup-työkalu helpottaa eri Rust-julkaisukanavien hallintaa sekä koko järjestelmässä että projektikohtaisesti. Oletuksena asennettuna on vakaa Rust. Voit asentaa nightly-version komennolla:

$ rustup toolchain install nightly

Voit tarkistaa asennetut Rust-versiot seuraavalla komennolla:

> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc

Jos haluat käyttää nightly-versiota vain tietyssä projektissa, voit tehdä sen seuraavasti:

$ cd ~/projects/needs-nightly
$ rustup override set nightly

Tämä varmistaa, että vain kyseisessä projektikansiossa käytetään nightly-versiota, vaikka järjestelmän oletus olisi vakaa Rust.

RFC-prosessi ja Rustin kehitysryhmät

Rustin kehitystä ohjaa RFC-prosessi (Request For Comments). Jos haluat parannuksia Rustiin, voit kirjoittaa RFC-ehdotuksen, jonka Rust-tiimi arvioi.

Rust-tiimi koostuu useista aihekohtaisista tiimeistä, kuten:

  • Kielisuunnittelu (language design)
  • Kääntäjän toteutus (compiler implementation)
  • Infra ja työkalut (infrastructure & tooling)
  • Dokumentaatio (documentation)

Täydellinen lista tiimeistä löytyy Rustin virallisilta sivuilta.

Jos RFC hyväksytään, siitä avataan GitHub-issue, jonka kuka tahansa voi toteuttaa. Kun toteutus valmistuu, se lisätään master-haaraan, mutta se pidetään ensin kokeellisena ominaisuutena feature flagin takana.

Kun tarpeeksi nightly-käyttäjiä on testannut ominaisuutta, Rust-tiimi arvioi, pitäisikö se tuoda vakaaseen Rustiin. Jos kyllä, feature flag poistetaan ja ominaisuus lisätään vakaaseen julkaisuun seuraavassa aikataulun mukaisessa versiossa.

Tämä prosessi varmistaa, että Rust kehittyy jatkuvasti ilman että vakaus kärsii – eli “stability without stagnation”.