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

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.