Smart Pointery w Rust – praktyczne wprowadzenie

Łukasz Andrzejewski
Pełnomocnik Zarządu ds. Rozwoju Edukacji
Ikona kalendarza
30 lipca 2025

W języku Rust zarządzanie pamięcią odbywa się przy pomocy unikalnego systemu własności i pożyczania. To właśnie ten system zapewnia bezpieczeństwo bez konieczności używania garbage collectora. Jednak w bardziej złożonych scenariuszach – takich jak struktury rekurencyjne, współdzielenie danych czy wielowątkowość – zwykłe referencje przestają wystarczać. W takich przypadkach z pomocą przychodzą smart pointery, które nie tylko wskazują na dane, ale często stają się ich właścicielami i zarządzają cyklem życia tych danych.

Box – podstawowy sposób przechowywania danych na stercie

Jednym z najprostszych smart pointerów w Rust jest Box. Umożliwia on przechowywanie wartości na stercie, zachowując jednocześnie bardzo niski narzut wydajnościowy. Sprawdza się doskonale, gdy mamy do czynienia z dużymi strukturami danych, które chcemy przenosić bez kopiowania.

Na przykład, deklarując prostą wartość:

let boxed_value = Box::new(42);

println!("Boxed value: {}", boxed_value);

umieszczamy liczbę całkowitą na stercie, a wskaźnik do niej trzymamy na stosie. Przy większych danych, takich jak tablice, Box pomaga ograniczyć zużycie pamięci stosu:

let large_array = Box::new([0; 1000000]);

Szczególnie ważne jest zastosowanie Box w strukturach rekurencyjnych, gdzie typ musi zawierać sam siebie. W przypadku prostego typu List:

enum List {

Cons(i32, Box<List>),

Nil,

}

każdy element listy zawiera kolejny element opakowany w Box, co pozwala kompilatorowi określić rozmiar typu.

Rc – współdzielona własność w jednym wątku

Gdy potrzebujemy, by kilka części programu współdzieliło dostęp do tej samej wartości, przydatny okazuje się Rc, czyli wskaźnik z liczonymi referencjami. Każde wywołanie Rc::clone zwiększa licznik i pozwala bezpiecznie korzystać z danych z wielu miejsc jednocześnie.

Rozważmy przykład, w którym tworzymy wartość i udostępniamy ją przez kilka referencji:

let data = Rc::new(String::from("Hello, Rc!"));

let reference1 = Rc::clone(&data);

let reference2 = Rc::clone(&data);

println!("Reference count: {}", Rc::strong_count(&data)); // Wydrukuje: 3

Dzięki Rc możliwe jest również tworzenie bardziej złożonych struktur danych, w których kilka gałęzi odnosi się do tych samych podstruktur:

enum List {

Cons(i32, Rc<List>),

Nil,

}

let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));

let b = Cons(3, Rc::clone(&a));

let c = Cons(4, Rc::clone(&a));

Każdy z elementów b i c dzieli strukturę a, co byłoby niemożliwe bez Rc.

RefCell – mutowalność mimo niemutowalnych referencji

Rust zazwyczaj wymaga, by dane były mutowalne tylko wtedy, gdy mamy do nich wyłączny dostęp. RefCell pozwala obejść to ograniczenie i wprowadza tzw. mutowalność wewnętrzną. Reguły pożyczania są nadal respektowane, ale sprawdzane dopiero w czasie działania programu.

Przykład pokazuje, jak można zmieniać wartość nawet przy niemutowalnej referencji:

let data = RefCell::new(42);

*data.borrow_mut() = 100;

println!("Value: {}", *data.borrow());

Co więcej, RefCell może być łączone z Rc, co daje możliwość współdzielenia mutowalnych danych:

let shared_data = Rc::new(RefCell::new(vec![1, 2, 3]));

let reference1 = Rc::clone(&shared_data);

let reference2 = Rc::clone(&shared_data);

reference1.borrow_mut().push(4);

reference2.borrow_mut().push(5);

println!("Shared data: {:?}", shared_data.borrow());

Taka kombinacja (Rc<RefCell>) jest niezwykle popularna w strukturach typu drzewo, które wymagają zarówno współdzielenia, jak i modyfikacji dzieci węzła.

Arc – współdzielenie danych między wątkami

Gdy zachodzi potrzeba udostępnienia tych samych danych w wielu wątkach, użycie Rc staje się niebezpieczne – nie jest bowiem bezpieczne dla wielu wątków. W takich przypadkach należy sięgnąć po Arc, czyli atomowy odpowiednik Rc.

Dzięki Arc możemy bezpiecznie współdzielić dane:

let data = Arc::new(vec![1, 2, 3, 4, 5]);

for i in 0..3 {

let data_clone = Arc::clone(&data);

thread::spawn(move || {

    println!("Thread {}: {:?}", i, data_clone);

});

}

Każdy z wątków otrzymuje kopię wskaźnika i może korzystać z danych bez ryzyka naruszenia bezpieczeństwa pamięci.

Mutex i RwLock – współdzielona mutowalność między wątkami

Aby móc nie tylko współdzielić dane między wątkami, ale również je modyfikować, należy użyć synchronizacji. Mutex pozwala na bezpieczny, sekwencyjny dostęp do zasobów:

let counter = Arc::new(Mutex::new(0));

for _ in 0..10 {

let counter_clone = Arc::clone(&counter);

thread::spawn(move || {

    let mut num = counter_clone.lock().unwrap();

    *num += 1;

});

}

Dzięki połączeniu Arc i Mutex, dane mogą być współdzielone i modyfikowane bez wyścigów danych, a każda operacja jest zabezpieczona blokadą.

Unikanie cykli i dobre praktyki

W przypadku intensywnego używania Rc, szczególnie w strukturach, gdzie obiekty nawzajem się referują (np. drzewo z odniesieniami do rodzica), należy uważać na cykle referencji. Rust nie posiada garbage collectora, więc cykl utworzony z Rc nigdy nie zostanie zwolniony. Rozwiązaniem jest stosowanie Weak, czyli słabej referencji:

struct Parent {

children: RefCell<Vec<Rc<Child>>>,

}

struct Child {

parent: Weak<Parent>,

}

Dzięki temu dziecko może wskazywać na rodzica, nie zwiększając jego liczby referencji – a zatem nie tworząc cyklu.

Podsumowanie

Smart pointery w Rust nie są tylko wygodą – są koniecznością w bardziej zaawansowanych konstrukcjach. Umożliwiają efektywne zarządzanie pamięcią, wspierają bezpieczeństwo i pozwalają na współdzielenie oraz mutowanie danych w kontrolowany sposób. Kluczem jest jednak właściwy dobór narzędzia do konkretnego problemu. Box sprawdza się przy prostym przechowywaniu na stercie, Rc przy współdzieleniu w jednym wątku, Arc w środowisku wielowątkowym, RefCell zapewnia mutowalność, a Mutex i RwLock kontrolę współbieżności. Odpowiednio wykorzystane – otwierają w Rust drzwi do potężnych, a przy tym bezpiecznych struktur danych.

Jeśli chcesz nauczyć się wykorzystywać te mechanizmy w praktyce, sprawdź szkolenie Programowanie w języku Rust, które wprowadza w świat bezpiecznego i nowoczesnego programowania systemowego.

Przeczytaj także

Ikona kalendarza

27 sierpień

Pożyczka na cyfryzację dla uczelni – jak sfinansować inwestycje IT już dziś

Trwa nabór na Pożyczkę na cyfryzację realizowaną w ramach Krajowego Planu Odbudowy przez Ministerstwo Cyfryzacji.

Ikona kalendarza

25 sierpień

Altmetric - nowoczesna analityka wpływu badań naukowych dla uczelni

Poznaj narzędzie Altmetric, które gromadzi i analizuje dane o odbiorze publikacji naukowych z tysięcy zewnętrznych źródeł, ułatwia ra...

Ikona kalendarza

13 sierpień

Sages realizuje projekt związany z dostępnścią cyfrową dofinansowany z Funduszy Europejskich

Sages realizuje projekt dofinansowany z Funduszy Europejskich z obszaru automatycznego zapewniania dostępności cyfrowych materiałów d...