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

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.