Rust on monipuolinen ohjelmointikieli
Vaikka Rustin suurimmat hyödyt tulevat esille ohjelmissa, joissa suorituskyky, sovellusten turvallinen monisäikeinen ajo sekä pieni muistijalanjälki ovat elintärkeitä, ja joissa esimerkiksi C ja C++ ovat olleet suosittuja vaihtoehtoja jo viimeiset liki 50 vuotta, on Rustista paljon muuhunkin. Muun muassa JavaScriptin ja Pythonin erilaisten kehittäjätyökalujen kirjoittaminen Rustilla on kasvattanut suosiotaan viime aikoina. Rustin laajan käyttäjäyhteisön ja suoraviivaisen paketinhallinnan myötä on Rustille luotu paketteja ja kirjastoja monenlaisiin käyttötarkoituksiin, aina web-ohjelmoinnista pelimoottoreihin ja työpöytäsovelluksiin. Näistä nostona uusi, nopeudestaan tunnettu, AI-avusteinen ja yhteistyöpohjainen koodieditori Zed, joka on kirjoitettu kokonaan Rustilla.
Rustin valttikortti on muistinhallinta
Rustin yksi keskeisimmistä ominaisuuksista on tehokas ja turvallinen muistinhallinta, joka perustuu muistin omistamisen, lainaamisen ja eliniän tarkkoihin sääntöihin, joita kääntäjä automaattisesti valvoo kääntämisvaiheessa. Omistamisen säännöt pähkinänkuoressa ovat:
- Jokaisen arvon omistaa jokin muuttuja.
- Jokaisella arvolla voi olla vain yksi omistaja kerrallaan.
- Kun arvon omistaja katoaa näkyvistä, arvo pudotetaan.
Esimerkkinä näistä säännöistä kaksi lyhyttä funktiota:
fn rules_one_and_two() {
let x: String = String::from("Sääntö 2: Jokaisella arvolla voi olla vain yksi omistaja kerrallaan.");
Luodaan muuttuja x
ja annetaan sille arvo, joka on tyyppiä String
.
let y = x;
Luodaan muuttuja y
ja määritetään sille x
:n arvo. String
on Rustissa tyyppi, jota ei ole ‘halpa’ kopioida (tällaisia ovat tyypit, joiden koko tiedetään kääntämisvaiheessa kuten erilaiset primitiivityypit bool
, i32
, char
…) joten y
:lle siirretään muuttujan x
omistama arvo.
println!("{x}");
}
Makro nimeltään println!
yrittää tulostaa stdout:iin muuttujan x
. Tämä kuitenkin epäonnistuu, sillä muuttujaa x
ei ole enää olemassa arvonsa poissiirtämisen jälkeen eikä sitä voi enää käyttää.
fn rule_three() {
let x = 10;
Määritetään uusi funktio ja sen näkymässä määritetään muuttujalle x
arvoksi 10.
{
println!("x: {x}");
let y = 50;
}
Määritetään kaarisuluilla uusi näkymä, jossa tulostetaan jo tutulla println!
-makrolla muuttujan x
arvo. Koska muuttuja on määritetty ulommassa näkymässä, se on olemassa myös tässä näkymässä ja tulostus tuottaa stdout:iin tekstin x: 10
. Määritetään tässä uudessa näkymässä myös muuttuja y
, annetaan sille arvoksi 50 ja suljetaan näkymä.
println!("y: {y}");
}
Funktion näkymässä muuttujaa y
ei ole olemassa ja tulostus epäonnistuu. Tämä tapahtuu siksi, että sisemmän näkymän päätyttyä y
on kadonnut näkyvistä ja sen arvo on pudotettu. Lopulta kun funktion näkymä päättyy, katoaa x
näkyvistä ja sen arvo putoaa. Muuttujia voi määrittää globaaleina (yleensä) tiedoston alussa näkymien ulkopuolelle, jolloin ne luonnollisesti näkyvät kaikissa näkymissä (eli funktioissa).
Rustin muuttujat (let x = 42
) ovat lähtökohtaisesti muuttumattomia. Muuttuja voidaan merkitä muuttuvaksi (let mut x = 42
), jolloin sen arvoa voidaan muokata määrityksen jälkeen, mutta vain mikäli sitä ei ole lainattu. Muuttujia voidaan ‘lainata’ toisiin muuttujiin käyttämällä referenssiä muuttujaan. Referenssejä on kahdenlaisia: muuttumattomia referenssejä (let y = &x
) tai muuttuvia referenssejä (let y = &mut x
). Muuttujalla voi olla samanaikaisesti joko rajaton määrä muuttumattomia referenssejä TAI tasan yksi muuttuva referenssi. Rustin kääntäjä seuraa lainausten elinaikoja tarkasti, joten viittauksia vapautettuun arvoon ei myöskään voi käyttää. Nämä säännöt tekevät rinnakkaisajon tuomista ongelmista (looking at you data races 👀) historiaa!
Esimerkiksi Javasta tuttua roskankeruuta ei Rustissa myöskään ole. Tämä mahdollistaa C/C++ -tasoisen suorituskyvyn ilman manuaalisen muistinhallinnan tuomia ’metkuja’ (nullpointterien tuoma undefined behaviour, buffer overflow, use-after-free anyone?), jotka satavarmasti ovat jossain välissä ilmaantuneet syvinä juonteina jokaisen C/C++ -kehittäjän otsalle. Mikäli ohjelmassa löytyy halua tai tarvetta sääntöjen rikkomiselle tai manuaaliselle muistinhallinnalle (Rust sisältää työkalut myös älykkäiden pointterien hallintaan), tyypillisesti esimerkiksi muiden kielten kanssa kommunikointiin tai erittäin matalan tason koodin kirjoittamiseen, myös tälle on keinonsa. Koodi voidaan määrittää unsafe
-avainsanalla. Unsafe tosin on ehdottomasti viimeinen vaihtoehto, jota ennen tulisi testata tarkkaan, onko muussa koodissa mitään vikaa, sillä muutkaan kääntäjän turvatoimet eivät tässä enää päde ja ohjelmasi virheiden ja kaatumisten riski kasvaa huomattavasti.
Tiukat säännöt luovat kynnyksen Rustin käyttämiselle
Jotta tämä kirjoitus ei menisi vain puhtaaksi ylistyslauluksi, puhutaan myös Rustin huonommistakin (onko niitä oikeasti?) puolista. Näistä suurimpana on oppimiskynnys ja koodin kirjoittamiseen kuluva aika verrattuna muihin, ’perinteisempiin’ koodikieliin. Kääntäjän valvomat omistajuus- ja elinaikasäännöt ovat sangen tiukat, joten ratkaisujen keksiminen kääntäjän miellyttämällä tavalla ei välttämättä ole suoraviivaista. Tämä johtaa myös siihen, ettei pienten, nopeiden koodinpätkien ja ohjelmien kirjoittaminen Rustilla ole kaikista mukavin kokemus. Myös kielen suhteellinen nuoruus kuvastuu kirjastojen kypsyydessä ja standardikirjaston ominaisuuksien laajuudessa. Mielestäni ehkä suurin näistä puutteista on hyvän GUI-kirjaston puute. Kielen nuoruus näkyy myös tekijöiden määrässä, sillä C- ja C++-osaajia on vuosien varrella ehtinyt kertymään jo reilusti, kun taas Rust-osaajia ei vielä niin monia ole. Mikäli Stack Overflow’n käyttäjäkyselystä voisi jotakin päätellä, kysyntä loisi myös tarjontaa tekijöistä, sillä halukkaita koodareita Rustia tekemään päivätyökseen olisi runsaasti.
Vaivannäkö palkitaan toimintavarmuudella
Olemassaolevan C- ja C++-koodin määrä maailmassa on valtava, eikä tätä kaikkea kukaan pysty, osaa tai halua kirjoittaa Rustilla uudestaan. Eikä tälle oikeastaan tarvetta olekaan, sillä vanhemman koodin bugit tulevat (pääsääntöisesti) korjatuksi ajan myötä. Rustin paikka mielestäni onkin juuri uusissa projekteissa, jossa etenkin koodin nopeudella ja muistiturvallisuudella on tärkeä rooli. Vaikka nykyaikainen C++ (etenkin versioista C++11/14/17 eteenpäin) onkin kehittynyt huomattavasti mm. muistinhallinnan (älykkäät pointterit ym.) ja moniajon (atomiset muuttujat, jopa STL sisältää niitä nykyään) saralla, on Rustilla ohjelman kirjoittamisen tuoman lisävaivan vastapainona kuitenkin Rust-kääntäjän takuu siitä, ettei määrittelemätöntä käytöstä tai kaatumisia tapahdu. Tämä vaihtokauppa on mielestäni täysin tekemisen arvoinen diili.
Parhaatkin kehittäjät tekevät varmasti virheitä ja bugeja eksyy jossain vaiheessa kaikkiin ohjelmiin. Miksi emme siis käyttäisi työkalua, joka auttaa havaitsemaan mahdolliset virheet jo koodia kirjoittaessa, eli mahdollisimman aikaisessa vaiheessa? Projektinhallinnan näkökulmasta tämä tulee halvemmaksi kuin bugien havaitseminen koodiarvioinnissa, joka tulee halvemmaksi kuin bugien havaitseminen (julkaisu)testaamisessa, joka taas tulee halvemmaksi kuin bugien havaitseminen tuotannossa…
Rust gud