Rust-yhteisössä on yleistä, että aloittelevat Rust-kehittäjät etsivät tapoja soveltaa tuttuja olio-ohjelmoinnin (OOP) konsepteja Rustin maailmaan. Vaikka Rust ei ole puhdas oliokieli, se tarjoaa tehokkaita työkaluja vastaavien ratkaisujen toteuttamiseen.

Tässä käydään läpi niitä olio-ohjelmoinnin ominaisuuksia, joita on turha etsiä Rustista.

Huomaa, että tässä keskitytään ominaisuuksiin, joita Rustissa ei ole. Rustissa on paljon ominaisuuksia, jotka tarjoavat samankaltaisia toiminnallisuuksia kuin oliokielissä, mutta niitä ei tarkemmin käsitellä tässä.

Rust on muistikeskeinen, ei toimintakeskeinen kieli

Rustin "objekti" on struct, eli tietue. Se on yksinkertaisesti muistialue, joka sisältää erilaisia kenttiä objektin kuvaamiseksi.

struct Henkilo {
    nimi: String,
    ika: u8,
}

Objektiin voidaan lisätä funktioita/metodeja tai funktioryhmiä, traits.

impl Henkilo {
    fn tervehdi(&self) {
        println!("Hei, olen {} ja olen {} vuotta", self.nimi, self.ika);
    }
}

Rustin erityispiirre on, että sekä structin määrittelijä että käyttäjä voivat lisätä siihen funktioita (ja traitteja). Jos esimerkiksi moduuli tarjoaa structin ilman debug-tulostusta (trait Debug), moduulin käyttäjä voi itse lisätä sen.

Funktioiden yhdistäminen structiin ei ole pakollista. Voit aivan hyvin luoda erillisiä funktioita, jotka ottavat structin parametrina.

fn tervehdi(henkilo: &Henkilo) {
    println!("Hei, olen {} ja olen {} vuotta", henkilo.nimi, henkilo.ika);
}

Perintä

Olio-ohjelmoinnin ja Rustin väliset erot alkavat näkyä perinnässä.

Olio-ohjelmoinnissa olion julkiset ominaisuudet pyritään usein toteuttamaan funktioina. Tämä mahdollistaa sen, että aliluokat voivat korvata perityt funktiot omalla toteutuksellaan. Jos esimerkiksi käytät suoraan muuttujaa kuten ika (u8), et voi muokata sen toimintaa aliluokassa. Käyttäjän on tiedettävä, että kyseessä on etumerkitön 8-bittinen kokonaisluku.

Suora muuttujan käyttö on kuitenkin huomattavasti tehokkaampaa kuin funktiokutsujen ketjun selvittäminen ajon aikana.

Esimerkiksi Henkilö-oliosta voisi olio-ohjelmoinnissa tehdä aliluokan, joka tietosuojasyistä kertoo iän vain asteikolla "alaikäinen"/"täysi-ikäinen". Jos käyttäjä lukee suoraan ika-muuttujan, tätä ei voi helposti toteuttaa. Metodin get_ika() avulla aliluokka voi kuitenkin muokata palautettavaa arvoa. Käyttäjän ei tarvitse tietää, että taustalla on 8-bittinen kokonaisluku, jota muokataan aliluokassa.

Olio-ohjelmointiin liittyy siis usein ylimääräistä työtä, joka ei ole välttämätöntä. Rust on tietoisesti jättänyt perinnän pois. Voit huoletta käyttää ika-kenttää suoraan. Jos haluat piilottaa tietoja, voit käyttää sisäkkäisiä struct-rakenteita, joissa piilotettava struct on julkisen structin sisällä. Rustin tyyppijärjestelmä tarjoaa tehokkaampia keinoja tietojen virheellisen käytön estämiseen, mutta niitä ei käsitellä tässä artikkelissa.

Rustissa ei tarvita gettereitä ja settereitä

Rustissa ei käytetä gettereitä ja settereitä. Näitä käytetään oliokielissä muuttamaan kentän arvo toiminnoiksi, joita voidaan perinnässä muokata.

Jos haluat muuttaa olion tilaa Rustissa, tee se suoraan.

fn main() {
    let mut henkilo = Henkilo {
        nimi: "Matti".to_string(),
        ika: 42,
    };
    henkilo.ika = 43;
    henkilo.tervehdi();
}

Seuraava oliokielissä tuikitavallien koodi siis osoittaa, ettet ole ymmärtänyt Rustin oliomallin perusteita.

struct Henkilo {
    nimi: String,
    ika: u8,
}

impl Henkilo {
    fn set_ika(&mut self, ika: u8) {
        self.ika = ika;
    }

    fn tervehdi(&self) {
        println!("Hei, nimeni on {} ja olen {} vuotta vanha.", self.nimi, self.ika);
    }
}

fn main() {
    let mut henkilo = Henkilo {
        nimi: "Matti".to_string(),
        ika: 42,
    };
    henkilo.set_ika(43);
    henkilo.tervehdi();
}

Ilman perintää gettereissä ja settereissä ei ole järkeä.

Private-kentät

Joissain olio-ohjelmointikielissä kenttien näkyvyyden määrittely on keskeinen osa suunnittelua. Kentät voivat olla yksityisiä, julkisia, aliluokkien käytettävissä tai oletusarvoisia, ja esimerkiksi Javassa on peräti 40 eri vaihtoehtoa käyttöpaikkojen ja suojausmääritelmien yhdistelmille.

Rustissa ei ole yksityisiä kenttiä. Kaikki kentät ovat moduulin sisällä julkisia. Tarkemmasta suojauksesta vastaa Rustin tyyppijärjestelmä.

Moduulien välillä Rustin funktiot ja kentät ovat oletuksena yksityisiä, mutta ne voidaan julkaista pub-avainsanalla.

Yleisesti ottaen Rustin kanssa voit unohtaa monimutkaiset näkyvyyssäännöt.

Rustissa ei ole new()-funktiota, joka loisi dynaamisia objekteja

Oliokielissä on usein konstruktoreita ja destruktoreita. Konstruktori luo olion ja destruktori poistaa sen. Rustista niitä on turha etsiä.

Rustissa ei ole erityistä new-konstruktoria, joka loisi olion. Ja erityisesti Rustissa ei ole new-konstruktoria, joka loisi dynaamisen olion.

Rust-olioita voi luoda aivan hyvin pinomuuttujina. Useimmat Rust-oliot eivät ole dynaamisia olioita, joille tila on varattava ajon aikana keosta.

fn main() {
    let henkilo = Henkilo {
        nimi: "Matti".to_string(),
        ika: 42,
    };
    henkilo.tervehdi();
}

Tämä olio käyttäytyy aivan kuin normaali muuttuja. Se on pinossa ja se poistetaan automaattisesti, kun sen käyttöalue päättyy. Sen koko tiedetään käännösaikana, joten se vain varataan pinosta ilman mitään muuta työtä.

Huomaa, että nimi on tyyppiä String, joka aina automaattisesti varataan keosta. Pinossa on siis vain viite kekoon tuon kentän osalta.

Jos olio luodaan dynaamisesti, se tehdään Boxin avulla.

fn main() {
    let henkilo = Box::new(Henkilo {
        nimi: "Matti".to_string(),
        ika: 42,
    });
    henkilo.tervehdi();
}

Dynaamiset muuttujat ovat aina sidoksissa pinomuuttujaan. Kun pinomuuttuja poistuu pinosta, siihen liittyvä dynaaminen muuttuja poistetaan automaattisesti.

Box käyttää standardikirjaston allokaattoria std::alloc::System, joka on oletuksena Linuxissa glibc:n malloc(). Sen voi kuitenkin korvata omalla allokaattorilla.

Rustissa on usein tapana tehdä new-funktio, joka luo olion. Mutta tämäkään ei ole dynaaminen olio.

struct Henkilo {
    nimi: String,
    ika: u8,
}

impl Henkilo {
    fn new(nimi: String, ika: u8) -> Henkilo {
        Henkilo { nimi, ika }
    }

    fn tervehdi(&self) {
        println!("Hei, nimeni on {} ja olen {} vuotta vanha.", self.nimi, self.ika);
    }
}

fn main() {
    let henkilo = Henkilo::new("Matti".to_string(), 42);
    henkilo.tervehdi();
}

Tämä esimerkki luo Henkilo-tyyppisen olion new()-funktiossa ja palauttaa sen main()-funktiolle. Paluuarvo kopioidaan main()-funktion henkilo-muuttujaan. Sille siis tehdään tilaa main()-funktion pinosta jo main()-funktion käynnistyessä.

Rustissa new()-funktio ei ole merkki dynaamisesta muistinvarauksesta. Sen käyttö on täysin vapaaehtoista. Kyse on tavallisesta käytännöstä, ei kielessä olevasta rakenteesta.

Rustissa ei puhuta patterneista

Patternit ovat oliokielten tapa selvitä ongelmista, joita oliomalli aiheuttaa.

Periaatteessa Rustissakin voisi olla patterneja, mutta kieli taipuu paljon paremmin selvittämään nuo ongelmat toisilla tavoilla.

Siksi: "First rule of Rust: Don't talk about patterns."