Zakaj Rust, če že imamo C in C++?
Kratek uvod v Rust. Zakaj preferiramo Rust.
Zakaj Rust, če že imamo C in C++?
C in C++ sta še vedno med najpomembnejšimi jeziki za sistemsko programiranje. Na njima stojijo operacijski sistemi, baze, brskalniki, gonilniki, embedded sistemi in ogromno druge infrastrukture.
Sta hitra, preverjena in omogočata zelo natančen nadzor nad pomnilnikom in strojno opremo.
Rust ne poskuša tega zamenjati z nečim popolnoma drugačnim. Poskuša obdržati podobno raven kontrole in performancea, hkrati pa zmanjšati cel razred napak, ki so pri C in C++ že desetletja normalen del razvoja.
Glavna razlika je compiler.
Rust compiler veliko stvari preveri še preden program sploh zaženeš.
Pri C lahko rezerviraš memory, ga sprostiš in kasneje po pomoti še vedno uporabljaš pointer nanj. Lahko pozabiš free(), dvakrat sprostiš isti memory ali pa ima več threadov hkrati dostop do podatkov na nevaren način.
Dober C ali C++ programer se temu zna izogniti. Problem je, da je velik del odgovornosti še vedno na človeku.
Rust del te odgovornosti prestavi na jezik in compiler.
Ownership
Ena glavnih idej Rusta je ownership.
Vsaka vrednost ima lastnika. Compiler spremlja, kdaj se ownership prenese, kdaj nekaj samo izposodimo in koliko časa je neka referenca veljavna.
Recimo:
fn main() {
let ime = String::from("Miha");
pozdravi(ime);
println!("{ime}");
}
fn pozdravi(ime: String) {
println!("Živjo {ime}");
}
Ta koda se ne prevede.
ime smo predali funkciji pozdravi, s tem pa se je ownership prenesel v funkcijo. Kasneje iste vrednosti ne moremo več uporabljati.
Če jo želimo samo prebrati, jo funkciji posodimo:
fn main() {
let ime = String::from("Miha");
pozdravi(&ime);
println!("{ime}");
}
fn pozdravi(ime: &String) {
println!("Živjo {ime}");
}
Tukaj funkcija dobi samo referenco. Ownership ostane v main in vrednost lahko uporabljamo tudi kasneje.
Na začetku je ta sistem naporen.
Pri večjem projektu pa postane zelo uporaben, ker mora biti precej jasno, kdo ima podatke, kdo jih lahko spreminja in kako dolgo sploh veljajo.
Memory safety brez garbage collectorja
Java, C#, Go in podobni jeziki velik del upravljanja pomnilnika rešujejo z garbage collectorjem.
C in C++ ti data več neposredne kontrole, s tem pa tudi več možnosti za memory bug.
Rust poskuša združiti oboje.
Nima klasičnega garbage collectorja, hkrati pa ti ne pusti kar prosto delati stvari, ki lahko vodijo do use-after-free, dangling pointerjev in podobnih problemov.
Ko vrednost ni več potrebna, se memory sprosti deterministično.
Compiler pa med prevajanjem preverja, ali so reference in ownership pravila še vedno veljavna.
Seveda lahko tudi v Rustu napišeš slab program.
Lahko narediš napačno business logiko, deadlock, memory leak, slab SQL ali slabo arhitekturo.
Rust ne rešuje vseh bugov.
Precej dobro pa zmanjšuje tiste, povezane z memory safetyjem in napačnim dostopom do podatkov.
Concurrency
Pri backendu hitro prideš do concurrencyja.
Več requestov teče hkrati. Več taskov uporablja skupne podatke. Delaš z connection pooli, cachem, queuei, background jobi in zunanjimi servisi.
Takrat postane zelo pomembno, kdo lahko bere ali spreminja isti podatek.
V C ali C++ moraš to zelo dobro nadzorovati sam.
Rust ima velik del teh pravil vgrajenih v type system.
Tipi, kot so Send in Sync, skupaj z ownershipom določajo, kaj se lahko varno premika ali deli med threadi.
Če compiler ne more dokazati, da je tak dostop varen, koda ne gre skozi.
To je precej velika razlika.
Pri C++ mora programer pogosto dokazati, da je nekaj varno.
Pri Rustu mora biti program napisan tako, da lahko varnost preveri compiler.
Error handling
Rust tudi precej eksplicitno obravnava napake.
Funkcija lahko vrne:
Result<T, E>
Kar pomeni: ali dobiš uspešen rezultat T ali napako E.
Če neka vrednost mogoče obstaja in mogoče ne, imaš:
Option<T>
Namesto da nekje dobiš null in šele kasneje ugotavljaš, zakaj je nekaj počilo, te type system prisili, da se s tem primerom ukvarjaš.
To pomeni več kode in več odločitev med razvojem.
Ampak tudi manj skritih predpostavk.
Zakaj Rust za backend
Za navaden CRUD API Rust ni vedno najbolj smiselna izbira.
Če moraš hitro postaviti servis, boš v Go, Java, C#, Pythonu ali TypeScriptu pogosto hitrejši.
Rust postane zanimiv, ko so pomembni performance, stabilen memory footprint, veliko concurrencyja, nizka latenca in dolgoročno vzdrževanje.
Backend proces lahko teče mesece ali leta. Vmes dobi milijone requestov, komunicira z bazami, queuei, cachem in drugimi servisi.
Takrat ni več pomembno samo, kako hitro napišeš prvo verzijo.
Pomembno je tudi, kaj se zgodi čez tri ali pet let, ko nekdo spremeni eno osnovno strukturo, skozi katero gre polovica sistema.
V manj strogem okolju lahko del posledic odkriješ šele v testih ali produkciji.
Pri Rustu spremeniš tip, ownership ali podpis funkcije in compiler pokaže mesta, kjer stara logika ne drži več.
To je več dela pri spremembi.
Hkrati pa dobiš precej natančen zemljevid stvari, ki jih moraš popraviti.
Rust zahteva več dela
Borrow checker se je treba naučiti.
Lifetimes znajo biti neprijetni.
Async Rust ima precej svoje kompleksnosti.
Generics in traiti lahko hitro postanejo zahtevni.
Compile time pri večjih projektih tudi ni majhen.
Za isto funkcionalnost boš prvo verzijo v kakšnem drugem jeziku pogosto napisal hitreje.
Rust ima pa drugačen trade-off.
Več težav poskuša rešiti med razvojem, namesto da jih kasneje loviš v testih ali produkciji.
Build error je poceni.
Production incident ni.
Kaj je Rust dejansko prinesel nad C in C++?
Ne nujno več performancea.
C in C++ sta že zelo hitra.
Rust prinese drugačno kombinacijo lastnosti:
- memory safety brez garbage collectorja,
- ownership in borrowing,
- precej varnejši concurrency,
- močan type system,
- eksplicitno error handling logiko,
- performance blizu C in C++,
- več napak ujetih pri compile time.
Cena je bolj zahteven razvoj.
Pri določenem tipu softwarea je to zelo dober kompromis.
Če pišeš sistem, ki mora biti hiter, stabilen in vzdrževan deset ali dvajset let, ti ni več tako pomembno, ali si prvi endpoint napisal v dveh urah ali v enem dnevu.
Bolj pomembno je, ali bo nekdo čez deset let lahko varno spremenil kodo, ne da bi pri tem podrl tri druge stvari.
Zato mi je Rust zanimiv predvsem za backend in infrastrukturo, kjer je dolgoročna zanesljivost pomembnejša od hitrosti prvega developmenta.
Malo pretirano:
1x napišeš, 100 let kompajlaš.
Ampak ideja je nekje tam.
#Rust #Backend #Programming #SystemsProgramming #SoftwareEngineering