Inteligentne wskaźniki w Rust: Box, Rc, Arc i RefCell w 2026

Inteligentne wskaźniki Rust Box, Rc, Arc i RefCell wyjaśnione na kompilowalnych przykładach z 2026, z tabelą decyzyjną i pytaniami rekrutacyjnymi.

Diagram zarządzania pamięcią dla inteligentnych wskaźników Rust Box, Rc, Arc i RefCell

Inteligentne wskaźniki w Rust to narzędzia, które odblokowują wzorce własności, jakich sam borrow checker nie potrafi wyrazić: alokację na stercie, współdzieloną własność oraz mutację za współdzieloną referencją. Podczas gdy zwykła referencja (&T) jedynie pożycza wartość, inteligentny wskaźnik jest właścicielem swoich danych i nakłada na nie dodatkowe zachowanie. Ten przewodnik omawia cztery typy, które każdy programista Rusta spotyka w produkcji i na rozmowach kwalifikacyjnych: Box, Rc, Arc i RefCell, wraz z kompilowalnymi przykładami dla edycji Rust 2024.

Podsumowanie w jednym zdaniu

Używaj Box<T> do alokacji na stercie z jednym właścicielem, Rc<T> do współdzielonej własności w obrębie jednego wątku, Arc<T> do współdzielonej własności między wątkami oraz RefCell<T> do mutacji wartości przez współdzieloną referencję. Pary Rc<RefCell<T>> i Arc<Mutex<T>> pokrywają współdzielony, modyfikowalny stan.

Czym są inteligentne wskaźniki w Rust

Inteligentny wskaźnik to struktura, która zachowuje się jak wskaźnik, ale niesie dodatkowe metadane lub możliwości. Większość implementuje cechę Deref, dzięki czemu *pointer oraz wywołania metod działają tak, jakby wskaźnik był zwykłą referencją, a także cechę Drop, dzięki czemu sprzątanie następuje automatycznie, gdy wartość opuszcza zasięg. Biblioteka standardowa dostarcza cztery typy omawiane tutaj, a ich zrozumienie zależy od solidnego opanowania własności i pożyczania, które decyduje o tym, kto i kiedy zwalnia każdą alokację.

Kluczową różnicą względem referencji jest własność. &T nigdy nie jest właścicielem danych, na które wskazuje, więc nie może przeżyć wartości. Inteligentny wskaźnik jest właścicielem danych, kontroluje ich czas życia i zwalnia je deterministycznie. Rozdział książki Rust o inteligentnych wskaźnikach traktuje je jako kategorię właśnie dlatego, że łączy je ten sam kształt: własność plus zachowanie.

Box<T>: alokacja na stercie dla typów rekurencyjnych i o znanym rozmiarze

Box<T> to najprostszy inteligentny wskaźnik. Przechowuje wartość na stercie, a na stosie trzyma wskaźnik do niej, z jednym właścicielem i bez narzutu w czasie wykonania poza samą alokacją. Jego najczęstszym zadaniem jest nadanie typom rekurencyjnym znanego rozmiaru: typ, który zawiera samego siebie bezpośrednio, byłby nieskończenie duży, ale Box to tylko wskaźnik, więc jego rozmiar jest stały niezależnie od tego, na co wskazuje.

Poniższy przykład definiuje drzewo binarne. Każdy Node przechowuje dwoje dzieci i bez pośredniości kompilator nie potrafi obliczyć rozmiaru Tree. Opakowanie każdego dziecka w Box przerywa rekurencję na poziomie typu.

tree.rsrust
#[derive(Debug)]
enum Tree {
    Leaf(i32),
    // Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
    Node(Box<Tree>, Box<Tree>),
}

fn sum(tree: &Tree) -> i32 {
    match tree {
        Tree::Leaf(value) => *value,                        // base case: return the leaf
        Tree::Node(left, right) => sum(left) + sum(right),  // recurse into both children
    }
}

fn main() {
    // Build (1) + ((2) + (3)) = 6
    let tree = Tree::Node(
        Box::new(Tree::Leaf(1)),
        Box::new(Tree::Node(
            Box::new(Tree::Leaf(2)),
            Box::new(Tree::Leaf(3)),
        )),
    );
    println!("sum = {}", sum(&tree)); // sum = 6
}

Box ma też znaczenie, gdy przenoszenie dużej wartości byłoby kosztowne, albo gdy zwracany jest obiekt cechy, taki jak Box<dyn Error>. W każdym przypadku zasada jest ta sama: jeden właściciel, zwalniany automatycznie, gdy Box zostaje usunięty.

Rc<T>: współdzielona własność w kodzie jednowątkowym

Czasem wartość potrzebuje kilku właścicieli i żaden z nich nie jest w oczywisty sposób ostatnim, który jej używa. Rc<T> (liczone referencje) rozwiązuje to, utrzymując silny licznik istniejących właścicieli. Rc::clone zwiększa ten licznik bez kopiowania danych bazowych, a każde usunięcie go zmniejsza. Gdy licznik osiąga zero, wartość zostaje zwolniona.

Przykładem jest obiekt konfiguracji odczytywany przez kilku pracowników. Każdy pracownik powinien trzymać konfigurację tak długo, jak jej potrzebuje, a alokacja powinna zniknąć dopiero wtedy, gdy zniknie ostatni pracownik.

shared_config.rsrust
use std::rc::Rc;

#[derive(Debug)]
struct Config {
    endpoint: String,
    timeout_ms: u32,
}

fn main() {
    // Rc::new moves Config onto the heap with a strong count of 1.
    let config = Rc::new(Config {
        endpoint: "https://api.example.com".to_string(),
        timeout_ms: 5000,
    });

    // Rc::clone only bumps the reference count; it does not deep-copy Config.
    let worker_a = Rc::clone(&config);
    let worker_b = Rc::clone(&config);

    // All three handles point to the same allocation.
    println!("endpoint: {}", worker_a.endpoint);
    println!("timeout:  {}", worker_b.timeout_ms);

    // strong_count reports how many owners currently hold the value.
    println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}

Jawne wywołanie Rc::clone (zamiast config.clone()) jest idiomatyczne: sygnalizuje czytelnikom, że operacja jest tania i dotyczy jedynie licznika. Haczyk polega na tym, że Rc<T> udostępnia wyłącznie współdzielone, niemutowalne referencje. Nie może mutować współdzielonej wartości i nie jest bezpieczny do przesyłania między wątkami. Dokumentacja Rc wyjaśnia oba ograniczenia.

RefCell<T> i wewnętrzna mutowalność w Rust

Rust zwykle wymusza, aby wartość była albo współdzielona przez wiele niemutowalnych referencji, albo mutowana przez dokładnie jedną mutowalną referencję, i sprawdza to w czasie kompilacji. RefCell<T> zapewnia wewnętrzną mutowalność: pozwala kodowi mutować wartość przez współdzieloną referencję, przenosząc to samo sprawdzenie na czas wykonania. borrow() zwraca współdzielony uchwyt do odczytu; borrow_mut() zwraca wyłączny uchwyt do zapisu. Reguły są identyczne, ale ich naruszenia powodują panikę zamiast błędu kompilacji.

W połączeniu z Rc daje to kanoniczny jednowątkowy wzorzec współdzielonego, mutowalnego stanu Rc<RefCell<T>>: kilku właścicieli, którzy wszyscy mogą aktualizować tę samą wartość.

counter.rsrust
use std::rc::Rc;
use std::cell::RefCell;

// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;

fn increment(counter: &SharedCounter) {
    // borrow_mut() hands out an exclusive reference, checked at runtime.
    *counter.borrow_mut() += 1;
}

fn main() {
    let counter: SharedCounter = Rc::new(RefCell::new(0));

    let handle_a = Rc::clone(&counter);
    let handle_b = Rc::clone(&counter);

    increment(&handle_a);
    increment(&handle_b);
    increment(&counter);

    // borrow() gives a shared read guard; the value is now 3.
    println!("count = {}", counter.borrow()); // count = 3
}
RefCell przenosi sprawdzanie pożyczeń na czas wykonania

Wywołanie borrow_mut(), gdy inny uchwyt borrow() lub borrow_mut() wciąż żyje, powoduje panikę z komunikatem already borrowed: BorrowMutError. Gwarancje bezpieczeństwa nadal obowiązują, ale błąd logiczny, który kompilator by wychwycił, staje się awarią w czasie wykonania. Uchwyty należy trzymać krótko i unikać przetrzymywania pożyczki przez wywołanie funkcji, które mogłoby ponownie wejść w ten sam RefCell.

Wewnętrzna mutowalność to również miejsce, gdzie kryją się cykle referencji. Dwie wartości Rc<RefCell<T>>, które wskazują na siebie nawzajem, na zawsze utrzymują swój silny licznik powyżej zera, więc żadna nigdy nie zostaje zwolniona. Rozwiązaniem jest Weak<T>, uchwyt bez własności, który nie wpływa na silny licznik; klasyczne omówienie znajduje się w Learn Rust With Entirely Too Many Linked Lists.

Arc<T>: bezpieczne wątkowo liczenie referencji dla współbieżności

Arc<T> (atomowo liczone referencje) to wielowątkowy odpowiednik Rc<T>. Publiczne API jest niemal identyczne, ale licznik używa operacji atomowych, dzięki czemu klonowanie i usuwanie Arc z kilku wątków naraz pozostaje poprawne. Ta atomowość jest powodem, dla którego Arc wybiera się tylko wtedy, gdy współdzielenie przekracza granicę wątku: atomowe zwiększanie licznika jest mierzalnie wolniejsze od zwykłego, którego używa Rc.

Ponieważ Arc<T> nadal udostępnia niemutowalne referencje, mutacja między wątkami wymaga prymitywu synchronizacji. Mutex<T> to zwykły partner, dający wzorzec Arc<Mutex<T>>, który odzwierciedla Rc<RefCell<T>> dla kodu współbieżnego. Przykład uruchamia cztery wątki, z których każdy tysiąc razy zwiększa współdzieloną sumę.

parallel_sum.rsrust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc is safe to share across threads; Mutex serializes access to the inner value.
    let total = Arc::new(Mutex::new(0u64));
    let mut handles = Vec::new();

    for _ in 0..4 {
        // Each thread gets its own Arc handle (an atomic count bump).
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // lock() blocks until the mutex is free, then returns a guard.
                let mut value = total.lock().unwrap();
                *value += 1;
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap(); // wait for every thread before reading
    }

    println!("total = {}", *total.lock().unwrap()); // total = 4000
}

Kompilator wymusza tę granicę: Rc<T> nie jest Send, więc próba przeniesienia go do thread::spawn nie skompiluje się, co kieruje ku Arc. Sięganie po Arc w obrębie jednego wątku to jedynie płacenie za atomy, których nigdy się nie używa. Dokumentacja Arc szczegółowo omawia gwarancje uporządkowania pamięci, a Arc<Mutex<T>> stanowi podstawę większości współdzielonego stanu w asynchronicznym Rust z Tokio.

Gotowy na rozmowy o Rust?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Box vs Rc vs Arc vs RefCell: kiedy używać którego

Te cztery typy komponują się wzdłuż dwóch osi: ilu właścicieli ma wartość oraz czy można ją mutować przez współdzielony uchwyt. Tabela podsumowuje kompromisy.

| Typ | Własność | Mutacja przez współdzielony uchwyt | Bezpieczny wątkowo | Narzut | |------|-----------|----------------------------|-------------|----------| | Box<T> | Pojedyncza | Nie | Tak, jeśli T: Send | Tylko alokacja na stercie | | Rc<T> | Współdzielona | Nie | Nie | Nieatomowy licznik | | Arc<T> | Współdzielona | Nie | Tak | Atomowy licznik | | RefCell<T> | Pojedyncza | Tak (sprawdzana w czasie wykonania) | Nie | Flaga pożyczenia w czasie wykonania |

Praktyczne drzewo decyzyjne jest krótkie. Potrzebny jeden właściciel na stercie: Box. Potrzebnych wielu właścicieli w jednym wątku: Rc. Potrzebnych wielu właścicieli między wątkami: Arc. Trzeba mutować współdzieloną wartość: należy opakować typ wewnętrzny w RefCell (jeden wątek) lub Mutex (wiele wątków). Wskazówki z Effective Rust dotyczące typów referencji i wskaźników prowadzą do tej samej warstwowości. Najlepiej zacząć od najmniej rozbudowanej opcji i dodawać możliwości dopiero wtedy, gdy wymusi to kompilator.

Pytania rekrutacyjne o inteligentne wskaźniki w Rust

Inteligentne wskaźniki to niezawodny temat na rozmowach kwalifikacyjnych, ponieważ sprawdzają, czy kandydat rozumie własność, a nie tylko składnię. Poniższe pytania pojawiają się często, a więcej z nich zebrano w module rekrutacyjnym o inteligentnych wskaźnikach w Rust.

Jaka jest różnica między Rc a Arc? Oba zapewniają współdzieloną własność przez liczenie referencji. Rc używa nieatomowego licznika i jest ograniczony do jednego wątku; Arc używa operacji atomowych i może być współdzielony między wątkami przy niewielkim koszcie w czasie wykonania. Kompilator wymusza ten podział: Rc nie jest ani Send, ani Sync, więc nie może przekroczyć granicy wątku.

Dlaczego łączy się Rc z RefCell? Rc daje współdzieloną własność, ale wyłącznie niemutowalny dostęp. RefCell dodaje wewnętrzną mutowalność, pozwalając właścicielom mutować wartość przez współdzieloną referencję ze sprawdzaniem pożyczeń w czasie wykonania. Razem Rc<RefCell<T>> stanowi idiomatyczny jednowątkowy budulec współdzielonego, mutowalnego stanu.

Jak liczenie referencji może powodować wyciek pamięci w Rust? Dwie wartości Rc (lub Arc), które odwołują się do siebie nawzajem, tworzą cykl, którego silne liczniki nigdy nie osiągają zera, więc alokacja nigdy nie zostaje zwolniona. Przerwanie cyklu za pomocą Weak<T>, który trzyma referencję bez własności, przywraca poprawne sprzątanie.

Kiedy Box jest lepszy od Rc? Zawsze, gdy wystarcza jeden właściciel. Box nie ma narzutu liczenia referencji, więc jest domyślnym wyborem do alokacji na stercie, typów rekurencyjnych i obiektów cech. Po Rc warto sięgać tylko wtedy, gdy potrzebna jest rzeczywista współdzielona własność.

Podsumowanie

Inteligentne wskaźniki w Rust zamieniają własność z ograniczenia w zestaw komponowalnych klocków. Wybór spośród nich wynika wprost z kształtu problemu:

  • Warto sięgnąć po Box<T>, gdy wartość potrzebuje jednego właściciela na stercie, typ rekurencyjny potrzebuje stałego rozmiaru albo funkcja zwraca obiekt cechy.
  • Rc<T> sprawdza się przy wielu właścicielach w jednym wątku, a przełączenie na Arc<T> następuje w momencie, gdy własność przekracza granicę wątku.
  • RefCell<T> dodaje wewnętrzną mutowalność w kodzie jednowątkowym, a Mutex<T> to jego współbieżny odpowiednik; uchwyty pożyczenia i blokady należy trzymać krótko.
  • Łączenie ich powinno być świadome: Rc<RefCell<T>> dla współdzielonego, mutowalnego stanu w jednym wątku, Arc<Mutex<T>> między wątkami.
  • Trzeba uważać na cykle referencji między liczonymi wskaźnikami i przerywać je za pomocą Weak<T>, aby uniknąć wycieków.
  • Domyślnie warto wybierać najmniej rozbudowany typ i pozwolić kompilatorowi wskazać, kiedy potrzebna jest większa moc.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#rust
#smart-pointers
#memory-management
#interview
#rust-2024-edition

Udostępnij

Powiązane artykuły