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

14 lipiec

Sztuczna inteligencja w świecie Spring

Świat oprogramowania dynamicznie wchodzi w nową erę – erę aplikacji wspieranych przez sztuczną inteligencję. W tej rzeczywistości Spr...

Ikona kalendarza

9 lipiec

Baza Usług Rozwojowych – jak skorzystać z dofinansowania na szkolenia?

Dowiedz się, jak zdobyć dofinansowanie na szkolenia Sages, dzięki Bazie Usług Rozwojowych. Sprawdź, kto może skorzystać!

Ikona kalendarza

25 czerwiec

Sposoby na przyspieszenie startu aplikacji działających na Wirtualnej Maszynie Javy

Długi czas uruchamiania aplikacji w Javie to częsty problem. Sprawdź, co go powoduje i jak wpływa na użycie Javy w środowiskach serve...