A Rust meghatározó eredménye a memóriabiztonság szemétgyűjtő (garbage collector) nélkül. Ezt egy fegyelmezett tulajdonlási rendszer segítségével éri el — amelynek azonban szándékosan van egy menekülési útvonala: a referenciaszámlálás.
A Rust-programban minden értéknek egyetlen tulajdonosa van egy adott pillanatban. Amikor a tulajdonos kikerül a hatókörből, az érték felszabadul — a memória determinisztikusan, az execúció egy ismert pontján szabadul fel. Nincs GC-szünet, nincs lógó mutató, nincs kettős felszabadítás. A fordítóprogram mindezt statikusan kényszeríti ki.
Ám bizonyos adatokat valóban meg kell osztani: egy gráf csúcsát több él is birtokolja, egy konfigurációs objektum sok alrendszeren át áramlik, egy visszahívás hivatkozást tart a körülvevő állapotra. Ide lép be a referenciaszámlálás — az Rc<T> és az Arc<T> —, amely a tulajdonlási logikát a fordítási időből egy kis futásidejű számlálóba helyezi át.
Ezek nem egymással versengő funkciók — egymást kiegészítő eszközök, eltérő kompromisszumokkal. Ez a cikk mindkettőt kibontja: szemantika, teljesítmény, ergonómia, és a döntő kérdés: melyiket válasszuk?
A tulajdonlási modell a Rust alapvető innovációja. Minden heap-allokációnak pontosan egy tulajdonosa van — az a változókötés, amelyik „tartja" azt. A tulajdonjog átruházható egy másik kötésre, ekkor az eredeti érvénytelenné válik. Sohasem másolódik csendben (hacsak a típus nem implementálja a Copy traitot).
fn main() { let s1 = String::from("helló"); let s2 = s1; // tulajdonjog ÁTRUHÁZVA — s1 mostantól érvénytelen // println!("{}", s1); ← fordítási hiba: érték mozgatás után kölcsönözve println!("{}", s2); // rendben — s2 az egyetlen tulajdonos } // s2 itt kerül felszabadításra, a memória automatikusan törlődik
A borrow checker érvényesíti a tulajdonlási szabályokat. Amikor s1-et s2-höz rendeljük, a fordító megérti, hogy s1 nem használható tovább — lemondott a tulajdonjogáról. Ez nullás futásidei költséggel szünteti meg a use-after-free hibákat.
A tulajdonjog mindenhová való átruházása kényelmetlen lenne. A Rust lehetővé teszi az értékek kölcsönzését: ideiglenes, hatókörbe zárt referencia felvételét a tulajdonjog átadása nélkül.
fn hossz_kiszamitas(s: &String) -> usize { s.len() } // s kikerül a hatókörből, de NEM szabadítja fel a String-et fn vilag_hozzaadasa(s: &mut String) { s.push_str(", világ"); } fn main() { let mut udvozles = String::from("helló"); let hossz = hossz_kiszamitas(&udvozles); // megosztott kölcsönzés vilag_hozzaadasa(&mut udvozles); // módosítható kölcsönzés println!("'{}' {} karakterből áll", udvozles, hossz); }
A borrow checker egyszerre kényszerít ki két invariánst:
&T) kölcsönzés élhet egyszerre — ezek csak olvashatók és nem aliasálhatnak mutációt.&mut T) kölcsönzés létezhet — és megosztott kölcsönzéssel nem lehet egyidejű.Ez az aliasálás VAGY mutálhatóság — egy alapvető szabály, amely statikusan szünteti meg a hibák egész kategóriáit (iterátor-invalidáció, adatversenyek).
A referenciák élettartam-annotációkkal egészülnek ki — ezek bizonyítják, hogy a referencia nem élheti túl azt az adatot, amelyre mutat. A legtöbb kódban a fordító kikövetkezteti ezeket; összetett generikus API-kban explicit módon kell megadni.
// 'a azt jelenti: a visszaadott referencia legalább annyit él, mint mindkét bemenet fn hosszabb<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() >= y.len() { x } else { y } }
Rc<T> és Arc<T>A tulajdonlás egy szigorú, egyetlen-tulajdonos modell. Ám egyes programok megosztott tulajdonlást igényelnek — a program több részének is életben kell tartania ugyanazt az értéket. A klasszikus példa egy gráf, ahol több él mutat ugyanarra a csúcsra.
Az Rc<T> (Reference Counted — Referenciaszámlált) egy értéket csomagol be a heap-en, két számlálóval együtt: egy erős számlálóval (aktív tulajdonosok) és egy gyenge számlálóval. Minden Rc-klón növeli az erős számlálót; minden drop csökkenti. Amikor az erős számláló nullára csökken, a belső érték felszabadul.
use std::rc::Rc; fn main() { let a = Rc::new(String::from("megosztott adat")); let b = Rc::clone(&a); // erős számláló növelése — olcsó mutatómásolás let c = Rc::clone(&a); println!("erős számláló = {}", Rc::strong_count(&a)); // 3 drop(b); println!("b eldobása után = {}", Rc::strong_count(&a)); // 2 } // a és c itt esik el; számláló → 0 → String felszabadul
Az Rc<T> megosztott tulajdonlást biztosít, de csak olvasható hozzáférést. A belső érték módosításához párosítani kell RefCell<T>-lel, amely a kölcsönzés-ellenőrzést fordítási időből futásidőre helyezi:
use std::rc::Rc; use std::cell::RefCell; fn main() { let megosztott = Rc::new(RefCell::new(vec![1, 2, 3])); let klon1 = Rc::clone(&megosztott); let klon2 = Rc::clone(&megosztott); klon1.borrow_mut().push(4); // futásidei kölcsönzés-ellenőrzés klon2.borrow_mut().push(5); println!("{:?}", megosztott.borrow()); // [1, 2, 3, 4, 5] }
Az Rc<T> nem Send vagy Sync — a számlálója nem atomikus, és nem léphet át szálhatárokon. Párhuzamos használathoz cseréljük le Arc<T>-re (Atomically Reference Counted — Atomikusan Referenciaszámlált), amely atomikus CPU-műveleteket használ a számlálóhoz. Szálak közötti belső módosíthatósághoz párosítsuk Mutex<T>-vel vagy RwLock<T>-lal.
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let szamlalo = Arc::new(Mutex::new(0u32)); let szalak: Vec<_> = (0..8).map(|_| { let sz = Arc::clone(&szamlalo); thread::spawn(move || { *sz.lock().unwrap() += 1; }) }).collect(); for szal in szalak { szal.join().unwrap(); } println!("számláló = {}", *szamlalo.lock().unwrap()); // 8 }
Rc<T> és az Arc<T> nem képes automatikusan felismerni a referenciaköröket. Ha két Rc érték kölcsönösen hivatkozik egymásra, az erős számlálóik soha nem érnek nullára, és memóriaszivárgás lép fel. Használjunk Weak<T>-t a visszamutató referenciákhoz (pl. szülőmutatók egy fában) a körök megtörésére.
| Dimenzió | Tulajdonlás & Kölcsönzés | Rc<T> / Arc<T> |
|---|---|---|
| Tulajdonlási modell | Egyetlen tulajdonos, szigorúan kikényszerítve | Megosztott tulajdonlás számlált handle-ökkel |
| Ellenőrzés | Fordítási idő — nulla futásidei költség | Futásidő — számláló-műveletek minden klónolás/felszabadításkor |
| Teljesítmény | Nulla többletköltség — nincs extra indirektció | Kis többletköltség: heap-allokáció, számláló, mutatókövetés |
| Szálbiztonság | Send/Sync által kényszerítve fordítási időben | Rc: csak egyetlen szál; Arc: szálbiztos |
| Módosíthatóság | &mut T — kizárólagos, statikusan ellenőrzött | RefCell/Mutex szükséges — futásidei panic lehetséges |
| Körök | N/A — egyetlen tulajdonos nem alkothat kört magával | Referenciakörök memóriaszivárgást okoznak — használj Weak<T>-t |
| API bonyolultsága | Meredekebb tanulási görbe (élettartamok) | Egyszerűbb felszín; a bonyolultság futásidőre kerül |
| Tipikus felhasználás | Alapértelmezett szinte minden adathoz Rust-ban | Gráfok, megosztott csúcsú fák, közös konfiguráció, visszahívások |
| Felszabadítás időzítése | Determinisztikus — a tulajdonos hatókörének végén | Determinisztikus — amikor az utolsó handle esik el (lehet nem nyilvánvaló) |
| Klónolási költség | Mély másolat (vagy mozgatás — ingyenes) | Mutatómásolat + számláló növelés — O(1) |
Az Rc<T> többletköltsége háromrétű: egy extra heap-allokáció a vezérlőblokknak, egy mutatókövetés minden hozzáférésnél, és két egész-növelés/csökkentés klónoláskor és felszabadításkor. A legtöbb program esetében ez elhanyagolható. Másodpercenként millió műveletet végző forró kódban — játékmotorok, fordítók, jelelfeldolgozók esetén — a tulajdonolt értékek előnyben részesítése számít.
Az Arc<T> további költséget jelent: az atomikus műveletek CPU memóriarendezési garanciákat (SeqCst vagy Release/Acquire) igényelnek, amelyek gátolnak bizonyos fordítói és hardveres átrendezéseket. Versenyző többmagos munkaterheléseknél ez mérhetővé válhat.
Rc/Arc-hoz, ha az alternatíva a borrow checkerrel való küzdelem összetett élettartam-annotációkkal vagy unsafe kóddal. A kis futásidei költség ergonomikus, biztonságos megosztott tulajdonlást ad cserébe — ez a legtöbb alkalmazáskódban ésszerű kompromisszum.
Arc<Mutex<T>>).// Klasszikus felhasználási eset: gráf megosztott csúcsokkal use std::rc::{Rc, Weak}; use std::cell::RefCell; struct Csucs { ertek: i32, gyerekek: Vec<Rc<RefCell<Csucs>>>>, szulo: Option<Weak<RefCell<Csucs>>>>, // Weak töri meg a szülő→gyerek kört } impl Csucs { fn uj(ertek: i32) -> Rc<RefCell<Csucs>> { Rc::new(RefCell::new(Csucs { ertek, gyerekek: vec![], szulo: None, })) } }