Rust smart pointer spiegati: Box, Rc, Arc e RefCell nel 2026

Gli smart pointer di Rust Box, Rc, Arc e RefCell spiegati con esempi compilabili del 2026, una tabella decisionale e le domande da colloquio più comuni.

Diagramma della gestione della memoria con gli smart pointer di Rust Box, Rc, Arc e RefCell

Gli smart pointer di Rust sono gli strumenti che sbloccano pattern di ownership che il borrow checker da solo non riesce a esprimere: allocazione sullo heap, possesso condiviso e mutazione dietro un riferimento condiviso. Dove un semplice riferimento (&T) si limita a prendere in prestito un valore, uno smart pointer possiede i propri dati e vi sovrappone un comportamento aggiuntivo. Questa guida analizza i quattro tipi che ogni sviluppatore Rust incontra in produzione e nei colloqui: Box, Rc, Arc e RefCell, con esempi compilabili per la edition Rust 2024.

Il riassunto in una riga

Box<T> serve per l'allocazione sullo heap con un solo proprietario, Rc<T> per il possesso condiviso su un singolo thread, Arc<T> per il possesso condiviso tra più thread e RefCell<T> per mutare un valore attraverso un riferimento condiviso. Le coppie Rc<RefCell<T>> e Arc<Mutex<T>> coprono lo stato mutabile condiviso.

Cosa sono gli smart pointer in Rust

Uno smart pointer è una struct che si comporta come un puntatore ma porta con sé metadati o capacità aggiuntive. La maggior parte implementa il trait Deref, così *pointer e le chiamate ai metodi funzionano come se il puntatore fosse un semplice riferimento, e il trait Drop, così la pulizia avviene automaticamente quando il valore esce dallo scope. La libreria standard fornisce i quattro tipi trattati qui, e comprenderli dipende da una solida padronanza di ownership e borrowing, che decide chi libera ogni allocazione e quando.

La distinzione fondamentale rispetto a un riferimento è il possesso. &T non possiede mai i dati a cui punta, quindi non può sopravvivere al valore. Uno smart pointer possiede i dati, ne controlla la durata e li libera in modo deterministico. Il capitolo del Rust Book sugli smart pointer li tratta come una categoria proprio perché condividono questa forma fatta di possesso più comportamento.

Box<T>: allocazione sullo heap per tipi ricorsivi e dimensionati

Box<T> è lo smart pointer più semplice. Memorizza un valore sullo heap e ne mantiene un puntatore sullo stack, con un unico proprietario e senza overhead a runtime oltre l'allocazione stessa. Il suo compito più comune è dare una dimensione nota ai tipi ricorsivi: un tipo che contiene sé stesso in modo diretto sarebbe infinitamente grande, ma una Box è solo un puntatore, quindi la sua dimensione è fissa a prescindere da ciò a cui punta.

L'esempio seguente definisce un albero binario. Ogni Node contiene due figli e, senza indirezione, il compilatore non può calcolare la dimensione di Tree. Racchiudere ogni figlio in una Box spezza la ricorsione a livello di tipo.

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 conta anche quando spostare un valore grande sarebbe costoso, o quando si restituisce un trait object come Box<dyn Error>. In ogni caso vale la stessa regola: un solo proprietario, liberato automaticamente quando la Box viene rilasciata.

Rc<T>: possesso condiviso nel codice a singolo thread

A volte un valore ha bisogno di più proprietari, nessuno dei quali è chiaramente l'ultimo a usarlo. Rc<T> (reference counted) risolve il problema mantenendo un conteggio forte di quanti proprietari esistono. Rc::clone incrementa quel conteggio senza copiare i dati sottostanti, e ogni rilascio lo decrementa. Quando il conteggio raggiunge zero, il valore viene liberato.

Si consideri un oggetto di configurazione letto da più worker. Ogni worker dovrebbe mantenere la configurazione finché ne ha bisogno, e l'allocazione dovrebbe sparire solo quando l'ultimo worker se ne va.

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
}

Chiamare Rc::clone in modo esplicito (invece di config.clone()) è idiomatico: segnala a chi legge che l'operazione è economica e tocca solo un contatore. Il rovescio della medaglia è che Rc<T> distribuisce soltanto riferimenti condivisi e immutabili. Non può mutare il valore che condivide e non è sicuro inviarlo tra i thread. La documentazione di Rc chiarisce entrambi i vincoli.

RefCell<T> e interior mutability in Rust

Rust normalmente impone che un valore sia condiviso tramite molti riferimenti immutabili oppure mutato tramite esattamente un riferimento mutabile, e lo verifica a tempo di compilazione. RefCell<T> offre la interior mutability: permette al codice di mutare un valore attraverso un riferimento condiviso spostando quella stessa verifica al runtime. borrow() restituisce una guardia di lettura condivisa; borrow_mut() restituisce una guardia di scrittura esclusiva. Le regole sono identiche, ma le violazioni causano un panic invece di far fallire la compilazione.

Combinato con Rc, questo produce il pattern canonico a singolo thread per lo stato mutabile condiviso, Rc<RefCell<T>>: più proprietari che possono tutti aggiornare lo stesso valore.

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 sposta il controllo dei prestiti al runtime

Chiamare borrow_mut() mentre un'altra guardia borrow() o borrow_mut() è ancora attiva causa un panic con already borrowed: BorrowMutError. Le garanzie di sicurezza reggono, ma un errore logico che il compilatore avrebbe intercettato diventa un crash a runtime. Conviene mantenere le guardie di breve durata ed evitare di trattenere un prestito attraverso una chiamata di funzione che potrebbe rientrare nella stessa RefCell.

La interior mutability è anche il punto in cui si nascondono i cicli di riferimento. Due valori Rc<RefCell<T>> che puntano l'uno all'altro tengono per sempre il conteggio forte dell'altro sopra lo zero, quindi nessuno dei due viene mai liberato. La soluzione è Weak<T>, un handle non proprietario che non incide sul conteggio forte; la spiegazione classica si trova in Learn Rust With Entirely Too Many Linked Lists.

Arc<T>: reference counting thread-safe per la concorrenza

Arc<T> (atomically reference counted) è il fratello multi-thread di Rc<T>. L'API pubblica è quasi identica, ma il contatore usa operazioni atomiche, così clonare e rilasciare un Arc da più thread contemporaneamente resta corretto. Questa atomicità è il motivo per cui Arc si sceglie solo quando la condivisione attraversa il confine di un thread: l'incremento atomico è misurabilmente più lento di quello semplice usato da Rc.

Poiché Arc<T> distribuisce comunque riferimenti immutabili, la mutazione tra thread richiede una primitiva di sincronizzazione. Mutex<T> è il partner abituale e dà il pattern Arc<Mutex<T>>, che rispecchia Rc<RefCell<T>> per il codice concorrente. L'esempio avvia quattro thread che incrementano ciascuno mille volte un totale condiviso.

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
}

Il compilatore impone il confine: Rc<T> non è Send, quindi provare a spostarlo dentro thread::spawn non compila, spingendo verso Arc. Ricorrere ad Arc all'interno di un singolo thread paga soltanto per operazioni atomiche mai usate. La documentazione di Arc tratta in dettaglio le garanzie di ordinamento della memoria, e Arc<Mutex<T>> sostiene gran parte dello stato condiviso in Rust asincrono con Tokio.

Pronto a superare i tuoi colloqui su Rust?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Box, Rc, Arc e RefCell a confronto: quando usare ciascuno

I quattro tipi si compongono lungo due assi: quanti proprietari ha un valore e se può essere mutato tramite un handle condiviso. La tabella riassume i compromessi.

| Tipo | Possesso | Mutazione tramite handle condiviso | Thread-safe | Overhead | |------|-----------|----------------------------|-------------|----------| | Box<T> | Singolo | No | Sì, se T: Send | Solo allocazione sullo heap | | Rc<T> | Condiviso | No | No | Conteggio non atomico | | Arc<T> | Condiviso | No | Sì | Conteggio atomico | | RefCell<T> | Singolo | Sì (verificata a runtime) | No | Flag di prestito a runtime |

L'albero decisionale pratico è breve. Serve un solo proprietario sullo heap: Box. Servono più proprietari su un singolo thread: Rc. Servono più proprietari tra più thread: Arc. Serve mutare un valore condiviso: racchiudere il tipo interno in RefCell (singolo thread) o Mutex (multi-thread). Le indicazioni di Effective Rust sui tipi riferimento e puntatore arrivano alla stessa stratificazione. Si parte dall'opzione meno potente e si aggiunge capacità solo quando il compilatore lo impone.

Rust smart pointer: domande da colloquio

Gli smart pointer sono un argomento affidabile nei colloqui perché verificano se un candidato comprende l'ownership e non solo la sintassi. Le domande seguenti ricorrono spesso, e altre sono raccolte nel modulo di colloquio sugli smart pointer di Rust.

Qual è la differenza tra Rc e Arc? Entrambi offrono possesso condiviso tramite reference counting. Rc usa un contatore non atomico ed è confinato a un singolo thread; Arc usa operazioni atomiche e può essere condiviso tra più thread a un piccolo costo a runtime. Il compilatore impone la separazione: Rc non è né SendSync, quindi non può attraversare il confine di un thread.

Perché combinare Rc con RefCell? Rc concede possesso condiviso ma solo accesso immutabile. RefCell aggiunge la interior mutability, permettendo ai proprietari di mutare il valore attraverso un riferimento condiviso con controlli sui prestiti a runtime. Insieme, Rc<RefCell<T>> è il blocco costitutivo idiomatico per lo stato mutabile condiviso a singolo thread.

Come può il reference counting causare memory leak in Rust? Due valori Rc (o Arc) che si riferiscono a vicenda formano un ciclo i cui conteggi forti non raggiungono mai zero, quindi l'allocazione non viene mai liberata. Spezzare il ciclo con Weak<T>, che mantiene un riferimento non proprietario, ripristina la pulizia corretta.

Quando è preferibile Box a Rc? Ogni volta che basta un solo proprietario. Box non ha overhead di reference counting, quindi è la scelta predefinita per l'allocazione sullo heap, i tipi ricorsivi e i trait object. Si ricorre a Rc solo quando serve un vero possesso condiviso.

Conclusione

Gli smart pointer di Rust trasformano l'ownership da vincolo a insieme di blocchi componibili. La scelta tra loro discende direttamente dalla forma del problema:

  • Usare Box<T> quando un valore ha bisogno di un unico proprietario sullo heap, un tipo ricorsivo ha bisogno di una dimensione fissa o una funzione restituisce un trait object.
  • Usare Rc<T> per più proprietari su un singolo thread e passare ad Arc<T> nel momento in cui il possesso attraversa il confine di un thread.
  • Aggiungere RefCell<T> per la interior mutability nel codice a singolo thread e Mutex<T> per l'equivalente concorrente, mantenendo brevi le guardie di prestito e di lock.
  • Combinarli con criterio: Rc<RefCell<T>> per lo stato mutabile condiviso su un singolo thread, Arc<Mutex<T>> tra più thread.
  • Fare attenzione ai cicli di riferimento tra puntatori con conteggio e spezzarli con Weak<T> per evitare leak.
  • Per impostazione predefinita ricorrere al tipo meno potente e lasciare che sia il compilatore a segnalare quando serve maggiore capacità.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

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

Condividi

Articoli correlati