Pradžia / Programavimas / Rust programavimas

Rust programavimas

Kodėl Rust apskritai verta dėmesio?

Jei kada nors programavote C ar C++, tikriausiai žinote tą jausmą – kai kodas atrodo tvarkingas, kompiliuojasi be klaidų, o paskui kažkur giliai sistemoje slypi atminties nutekėjimas arba dangling pointer, kuris išlenda tik po kelių savaičių. Rust buvo sukurtas kaip atsakas į šią problemą. Ne kaip dar viena aukšto lygio kalba su šiukšlių surinkėju, o kaip kažkas visiškai kitokio – sistema, kuri leidžia rašyti žemo lygio kodą, bet su garantijomis, kurių anksčiau tiesiog nebuvo.

Mozilla Research pradėjo Rust projektą 2006 metais, o pirmoji stabili versija pasirodė 2015-aisiais. Nuo tada kalba tapo viena labiausiai mylimų tarp programuotojų – Stack Overflow apklausose Rust jau keletą metų iš eilės laimi „labiausiai mėgstamos programavimo kalbos” titulą. Tai nėra atsitiktinumas.

Rust nėra lengva kalba. Tai reikia pasakyti iš karto, nes daug žmonių pradeda mokytis ir po poros savaičių nusivilia. Borrow checker – tai ta Rust dalis, kuri tikrina atminties naudojimą kompiliavimo metu – iš pradžių atrodo kaip priešas. Bet kai supranti, kodėl jis egzistuoja, viskas keičiasi.

Nuosavybė, skolinimas ir gyvavimo laikotarpiai – Rust širdis

Rust atminties valdymo modelis remiasi trimis pagrindinėmis sąvokomis: ownership (nuosavybė), borrowing (skolinimas) ir lifetimes (gyvavimo laikotarpiai). Šios trys sąvokos kartu sudaro sistemą, kuri leidžia kompiliatoriui garantuoti atminties saugumą be jokio runtime overhead.

Nuosavybės taisyklė paprasta: kiekviena reikšmė Rust programoje turi vieną savininką. Kai savininkas išeina iš scope, reikšmė automatiškai atlaisvinama. Štai paprastas pavyzdys:


fn main() {
    let s1 = String::from("labas");
    let s2 = s1; // s1 nebegalioja, nuosavybė perduota s2
    println!("{}", s2); // veikia
    // println!("{}", s1); // kompiliavimo klaida!
}

Tai atrodo keistai, jei esate įpratę prie kitų kalbų. Python ar JavaScript tiesiog nukopijuotų nuorodą. Rust sako – ne, nuosavybė yra viena, ir ji perduodama, ne dalijamasi.

Skolinimas leidžia perduoti nuorodą į reikšmę, neperduodant nuosavybės. Galite turėti arba vieną mutable nuorodą, arba kiek norite immutable nuorodų – bet ne abu variantus tuo pačiu metu. Tai eliminuoja data race sąlygas kompiliavimo lygmenyje, dar prieš paleidžiant programą.

Gyvavimo laikotarpiai yra sudėtingiausia dalis. Jie nurodo kompiliatoriui, kiek laiko nuoroda turi galioti. Dažniausiai kompiliatorius juos išveda pats (lifetime elision), bet kartais reikia nurodyti eksplicitiškai:


fn ilgiausias<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Čia 'a sako: grąžinama nuoroda gyvuos tiek, kiek trumpiau gyvuojanti iš dviejų įvesties nuorodų. Tai gali atrodyti painiai, bet tai yra kompiliatoriaus būdas užtikrinti, kad niekada negrąžinsite nuorodos į jau atlaisvintą atmintį.

Cargo – įrankis, kuris keičia žaidimo taisykles

Vienas dalykų, kuris Rust daro tokį malonų naudoti praktikoje, yra Cargo – oficialus paketų valdytojas ir build sistema. Jei dirbote su C++ ir CMake, žinote, koks gali būti skausmas. Rust su Cargo tai išsprendė elegantiškai.

Naujas projektas sukuriamas viena komanda:

cargo new mano_projektas
cd mano_projektas
cargo run

Cargo automatiškai sukuria projekto struktūrą, Cargo.toml failą su priklausomybėmis ir src/main.rs. Priklausomybės pridedamos paprastai – Cargo.toml faile nurodote biblioteką ir versiją, o Cargo ją atsisiunčia iš crates.io (oficialus Rust paketų registras) ir sukompiliuoja.

Cargo taip pat integruoja testavimą (cargo test), dokumentacijos generavimą (cargo doc), kodo formatavimą (cargo fmt) ir statinę analizę (cargo clippy). Clippy yra ypač vertingas – tai linter, kuris ne tik randa klaidas, bet ir moko geriau rašyti Rust kodą, siūlydamas idiomatiškesnius sprendimus.

Praktinis patarimas: visada naudokite cargo clippy prieš commit’indami kodą. Ir nustatykite cargo fmt kaip pre-commit hook – taip visi komandos nariai rašys vienodai formatuotą kodą be jokių diskusijų apie skliaustus ir tarpus.

Traits ir generics – Rust polimorfizmas

Rust neturi klasių ir paveldėjimo tradicine prasme. Vietoj to yra struktūros (struct), enumeracijos (enum) ir traits. Traits yra panašūs į interfeisus kitose kalbose, bet galingesni.

Trait apibrėžia elgseną, kurią tipas gali implementuoti:


trait Greet {
    fn sveikink(&self) -> String;
    
    fn sveikink_garsiai(&self) -> String {
        self.sveikink().to_uppercase() // default implementacija
    }
}

struct Lietuvis {
    vardas: String,
}

impl Greet for Lietuvis {
    fn sveikink(&self) -> String {
        format!("Labas, {}!", self.vardas)
    }
}

Generics leidžia rašyti kodą, kuris veikia su skirtingais tipais, bet su trait bounds – apribojimais, nurodančiais, kokias savybes tas tipas turi turėti:


fn spausdink_sveikinima<T: Greet>(objektas: &T) {
    println!("{}", objektas.sveikink());
}

Svarbu suprasti, kad Rust generics naudoja monomorphization – kompiliavimo metu kiekvienam naudojamam tipui sugeneruojamas atskiras kodo variantas. Tai reiškia zero-cost abstractions – jokio runtime overhead lyginant su rankiniu kodo rašymu kiekvienam tipui atskirai.

Rust standartinėje bibliotekoje yra daug naudingų traits: Display ir Debug spausdinimui, Clone ir Copy kopijavimui, Iterator iteravimui, From ir Into konvertavimui. Išmokite juos – jie yra Rust idiomatikos pagrindas.

Klaidų valdymas be išimčių

Rust neturi išimčių (exceptions). Tai iš pradžių gali šokiruoti, bet po kurio laiko supranti, kad tai yra vienas geriausių kalbos sprendimų. Vietoj išimčių Rust naudoja du tipus: Option<T> reikšmėms, kurių gali nebūti, ir Result<T, E> operacijoms, kurios gali nepavykti.


use std::fs;
use std::io;

fn skaityk_faila(kelias: &str) -> Result<String, io::Error> {
    let turinys = fs::read_to_string(kelias)?;
    Ok(turinys)
}

fn main() {
    match skaityk_faila("duomenys.txt") {
        Ok(turinys) => println!("Failas: {}", turinys),
        Err(klaida) => eprintln!("Klaida: {}", klaida),
    }
}

Klaustukas (?) operatorius yra sintaksinis cukrus – jei Result yra Err, jis grąžina klaidą iš funkcijos. Tai daro klaidų propagavimą labai patogų, bet kartu priverstinį – negalite tiesiog ignoruoti klaidos, kaip galima padaryti su išimtimis.

Praktinis patarimas: realiuose projektuose naudokite anyhow arba thiserror bibliotekas klaidų valdymui. thiserror puikiai tinka bibliotekoms – leidžia apibrėžti savo klaidų tipus su aiškiais pranešimais. anyhow geriau tinka aplikacijoms – supaprastina klaidų propagavimą, kai tikslus klaidos tipas nėra kritiškai svarbus.

Dar vienas patarimas: vengite unwrap() produkciniame kode. unwrap() sukelia panic, jei reikšmė yra None arba Err – tai yra greitas būdas programai crashinti. Naudokite expect() su aiškiu pranešimu bent jau debuginimui, arba geriau – tinkamai apdorokite klaidą.

Async Rust – galingas, bet sudėtingas

Asinchroninis programavimas Rust yra viena sudėtingiausių, bet kartu galingiausių kalbos dalių. Rust async/await sintaksė atrodo panaši į JavaScript ar Python, bet po gaubtu viskas veikia kitaip.

Rust async funkcijos grąžina Future – tai yra lazy computation, kuri nevykdoma tol, kol ją nepoll’ina executor. Pats Rust neturi built-in async runtime – reikia pasirinkti biblioteką. Šiuo metu de facto standartas yra Tokio:


use tokio;

#[tokio::main]
async fn main() {
    let rezultatas = atlik_uzduoti().await;
    println!("Rezultatas: {}", rezultatas);
}

async fn atlik_uzduoti() -> String {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    String::from("Atlikta!")
}

Tokio yra puikus pasirinkimas serverio aplikacijoms – HTTP serveriai, duomenų bazių klientai, WebSocket serveriai. Alternatyva – async-std, kuri stengiasi būti artimesnė standartinei bibliotekai.

Svarbu žinoti: async Rust turi savo borrow checker niuansų. Futures turi būti Send, jei norite jas siųsti tarp gijų. Tai reiškia, kad visi tipai, naudojami async kontekste, taip pat turi būti Send. Dažna klaida – bandyti naudoti Rc (ne thread-safe) vietoj Arc (thread-safe) async kode.

Praktinis patarimas: jei pradedate naują web projektą Rust, pažiūrėkite į Axum – web framework’as, sukurtas ant Tokio, su labai ergonomiška API. Alternatyvos – Actix-web (labai greitas, bet sudėtingesnis) ir Warp.

Rust sisteminėje programuotojoje ir WebAssembly

Rust pradžioje buvo orientuotas į sisteminį programavimą – operacinių sistemų komponentai, įterptinės sistemos, žaidimų varikliai. Ir šioje srityje jis tikrai šviečia. Linux branduolys nuo 2022 metų oficialiai priima Rust kodą – tai milžiniškas žingsnis, nes Linux iki tol buvo beveik išimtinai C kalba.

Įterptinėse sistemose Rust veikia be standartinės bibliotekos (no_std režimas) – tai leidžia naudoti jį mikrovaldikliuose su keliais kilobaitais atminties. Projektai kaip Embassy leidžia rašyti async kodą mikrovaldikliams – tai buvo neįsivaizduojama prieš kelerius metus.

WebAssembly yra kita sritis, kur Rust dominuoja. wasm-pack įrankis leidžia kompiliuoti Rust kodą į WebAssembly ir naudoti jį JavaScript projektuose. Tai ypač naudinga, kai reikia didelio našumo skaičiavimų naršyklėje – vaizdo apdorojimas, kriptografija, žaidimai.


// Rust funkcija, kuri bus iškviesta iš JavaScript
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn sudeti(a: u32, b: u32) -> u32 {
    a + b
}

Cloudflare Workers, Fastly Compute@Edge ir kiti edge computing sprendimai taip pat naudoja WebAssembly, ir Rust yra vienas populiariausių pasirinkimų šioje srityje dėl mažo binarinio failo dydžio ir greičio.

Kur eiti toliau – ir kodėl verta kentėti mokantis

Rust mokymosi kreivė yra tikra. Pirmosios savaitės su borrow checker gali būti frustracijos pilnos. Bet štai kas nutinka po kelių mėnesių: pradedate galvoti apie atmintį, nuosavybę ir gijų saugumą visiškai kitaip. Net kai grįžtate prie Python ar JavaScript, pastebite klaidas, kurių anksčiau nepastebėdavote.

Geriausias mokymosi kelias: pradėkite nuo oficialios knygos – The Rust Programming Language (dar vadinama „The Book”), prieinama nemokamai online. Po to – Rustlings, interaktyvūs pratimai, kurie priverčia iš tikrųjų rašyti kodą, o ne tik skaityti. Exercism.io turi puikų Rust treką su mentoriais.

Realiam projektui rekomenduoju pradėti nuo komandinės eilutės įrankio – tai leidžia susipažinti su Rust be async ir web sudėtingumo. clap biblioteka argumentų parsavimui, serde JSON/YAML serializacijai – šios dvi bibliotekos bus beveik kiekviename projekte.

Rust ekosistema auga greitai. AWS, Google, Microsoft, Meta – visos šios kompanijos investuoja į Rust. Android operacinė sistema vis daugiau naujų komponentų rašo Rust. Windows branduolys taip pat pradeda priimti Rust kodą. Tai nėra hobistų kalba – tai kalba, kuri keičia kaip industija galvoja apie sisteminį programavimą.

Gal svarbiausia: Rust verčia jus būti tikslius. Negalite tiesiog „tikėtis, kad veiks” – kompiliatorius reikalauja, kad suprastumėte, ką darote. Iš pradžių tai erzina. Vėliau supranti, kad tai yra didžiausia kalbos dovana.