Weak e Cow in Rust
Nella pagina sugli smart pointer hai conosciuto Box, Rc e RefCell. Questa pagina aggiunge altri due strumenti che risolvono due problemi molto concreti. Weak<T> ti permette di puntare a un valore senza tenerlo in vita. Cow<T> permette a una funzione di restituire dati presi in prestito quando può, e di fare una copia solo quando è proprio necessario.
Pensa a Rc come alla chiave di una casa: finché qualcuno ha una chiave, la casa resta in piedi. Weak assomiglia di più all’indirizzo scritto su un foglietto. Puoi usarlo per trovare la casa ma, se tutti restituiscono le chiavi e la casa non c’è più, l’indirizzo non porta da nessuna parte. Nessuno resta a pagare per una casa di cui si era dimenticato.
Il problema: i cicli di Rc non vengono mai liberati
Sezione intitolata “Il problema: i cicli di Rc non vengono mai liberati”Un valore Rc viene eliminato quando il suo conteggio forte (strong count) arriva a 0. Funziona benissimo finché due valori non tengono un Rc l’uno all’altro. A quel punto ognuno tiene in vita l’altro, per sempre.
Per vederlo, il tipo Person qui sotto implementa Drop, così stampa un messaggio quando viene liberato. Guarda quali messaggi compaiono:
use std::cell::RefCell;use std::rc::Rc;
struct Person { name: String, friend: RefCell<Option<Rc<Person>>>,}
impl Drop for Person { fn drop(&mut self) { println!("elimino {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice punta a bob, e bob punta di nuovo ad alice *alice.friend.borrow_mut() = Some(Rc::clone(&bob)); *bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("strong count di alice: {}", Rc::strong_count(&alice)); println!("strong count di bob: {}", Rc::strong_count(&bob)); println!("fine di main");}Output:
strong count di alice: 2strong count di bob: 2fine di mainNota cosa manca: non c’è nessun elimino alice e nessun elimino bob. Quando main finisce, le variabili alice e bob spariscono, quindi ogni conteggio scende da 2 a 1. Però non arriva mai a 0, perché ogni persona è ancora tenuta dal campo friend dell’altra. Quella memoria è persa (leak).
Non preoccuparti se la cosa ti sorprende. Il compilatore non può accorgersene, e non è un crash. È solo memoria che non torna più indietro. In un programma che gira a lungo e continua a creare cicli, si accumula.
Weak<T>: un puntatore che non tiene in vita
Sezione intitolata “Weak<T>: un puntatore che non tiene in vita”La soluzione è rendere deboli (weak) uno o entrambi i collegamenti. Ottieni un Weak da un Rc con Rc::downgrade. Un collegamento debole non conta come proprietario, quindi non impedisce che il valore venga eliminato.
Visto che il valore potrebbe non esserci già più, non puoi usare un Weak direttamente. Chiami .upgrade(), che ti dà un Option<Rc<T>>: Some se il valore è ancora vivo, None se non lo è.
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Person { name: String, friend: RefCell<Weak<Person>>,}
impl Drop for Person { fn drop(&mut self) { println!("elimino {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob); *bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() { println!("l'amico di alice è {}", friend.name); }
println!("alice: strong = {}, weak = {}", Rc::strong_count(&alice), Rc::weak_count(&alice)); println!("fine di main");}Output:
l'amico di alice è bobalice: strong = 1, weak = 1fine di mainelimino bobelimino aliceQuesta volta compaiono entrambe le righe elimino. Alice ha un proprietario forte (la variabile alice) e un collegamento debole (quello di Bob). Quando main finisce, il conteggio forte arriva a 0 e Rust la libera. Bob se ne va per primo perché le variabili vengono eliminate in ordine inverso rispetto alla creazione.
Alcune cose da ricordare:
Weak::new()crea un puntatore debole vuoto, che non punta a niente.upgrade()su di lui restituisce sempreNone. È un comodo valore di partenza.Rc::strong_countconta i proprietari.Rc::weak_countconta i collegamenti deboli. Solo il conteggio forte decide quando il valore viene eliminato.- L’
Rcche ottieni daupgrade()è un vero proprietario, temporaneo. Quando esce dallo scope, il conteggio forte torna a scendere.
Un albero di cartelle: i figli si possiedono, i genitori sono deboli
Sezione intitolata “Un albero di cartelle: i figli si possiedono, i genitori sono deboli”Gli alberi sono il posto classico per Weak. Una cartella possiede le sue sottocartelle, quindi children contiene degli Rc. Una sottocartella vuole anche conoscere il suo genitore, ma non dovrebbe possederlo. Se entrambe le direzioni usassero Rc, avresti di nuovo un ciclo. Quindi parent è un Weak:
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Folder { name: String, parent: RefCell<Weak<Folder>>, children: RefCell<Vec<Rc<Folder>>>,}
fn new_folder(name: &str) -> Rc<Folder> { Rc::new(Folder { name: name.to_string(), parent: RefCell::new(Weak::new()), children: RefCell::new(Vec::new()), })}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child);}
fn full_path(folder: &Folder) -> String { match folder.parent.borrow().upgrade() { Some(parent) => format!("{}/{}", full_path(&parent), folder.name), None => folder.name.clone(), }}
fn main() { let home = new_folder("casa"); let photos = new_folder("foto"); let trips = new_folder("viaggi");
add_child(&home, Rc::clone(&photos)); add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() { Some(parent) => println!("foto sta in {}", parent.name), None => println!("foto non ha un genitore"), }
println!("percorso completo: {}", full_path(&trips)); println!("casa: strong = {}, weak = {}", Rc::strong_count(&home), Rc::weak_count(&home)); println!("foto: strong = {}, weak = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));}Output:
foto sta in casapercorso completo: casa/foto/viaggicasa: strong = 1, weak = 1foto: strong = 2, weak = 1Leggi i conteggi con calma. photos ha due proprietari forti: la variabile photos e la lista children dentro home. Ha un collegamento debole: il campo parent di trips. home ha un solo proprietario forte, la sua variabile, perché nessuno possiede il proprio genitore. È proprio la forma che vuoi: l’ownership scorre verso il basso nell’albero, e i collegamenti che vanno verso l’alto sono deboli.
RefCell serve perché imposti parent e aggiungi a children quando le cartelle sono già dentro un Rc, e Rc da solo ti dà solo un accesso condiviso in sola lettura.
Liste deboli che non tengono in vita niente
Sezione intitolata “Liste deboli che non tengono in vita niente”Weak è ottimo anche per le liste del tipo «tieni d’occhio queste cose, ma non trattenerle»: listener, observer o una cache di cose che appartengono a qualcun altro. Qui il punteggio di un gioco manda aggiornamenti a chiunque stia ancora ascoltando. Quando un listener viene eliminato, il suo collegamento debole smette semplicemente di funzionare:
use std::rc::{Rc, Weak};
struct Listener { name: String,}
fn notify(listeners: &[Weak<Listener>], event: &str) { for weak in listeners { match weak.upgrade() { Some(listener) => println!("{} ha ricevuto: {event}", listener.name), None => println!("(un listener non c'è più, lo salto)"), } }}
fn main() { let screen = Rc::new(Listener { name: String::from("schermo") }); let logger = Rc::new(Listener { name: String::from("logger") });
let mut listeners: Vec<Weak<Listener>> = Vec::new(); listeners.push(Rc::downgrade(&screen)); listeners.push(Rc::downgrade(&logger));
notify(&listeners, "punteggio 10");
drop(logger); // il logger viene chiuso dal suo proprietario
notify(&listeners, "punteggio 20");
// fai pulizia dei collegamenti che non puntano a niente listeners.retain(|weak| weak.upgrade().is_some()); println!("listener rimasti: {}", listeners.len());}Output:
schermo ha ricevuto: punteggio 10logger ha ricevuto: punteggio 10schermo ha ricevuto: punteggio 20(un listener non c'è più, lo salto)listener rimasti: 1La lista non ha mai impedito che il logger venisse eliminato. Se avesse contenuto degli Rc, il logger sarebbe sopravvissuto dentro la lista, continuando a ricevere messaggi che nessuno voleva. Con Weak è il proprietario a decidere quando una cosa sparisce, e tutti gli altri si limitano a controllare con upgrade().
Cow: prendi in prestito quando puoi, copia quando devi
Sezione intitolata “Cow: prendi in prestito quando puoi, copia quando devi”Ora un problema diverso. Diciamo che scrivi una funzione che ripulisce i nomi utente. La maggior parte dei nomi va già bene. Alcuni hanno spazi in più o lettere maiuscole. Se la tua funzione restituisce sempre una nuova String, fai una copia di ogni singolo nome, anche di quelli che non hai modificato.
Cow («clone on write», copia alla scrittura, da std::borrow::Cow) risolve il problema. Un Cow<'_, str> è un enum con due varianti:
Cow::Borrowed(&str): «ecco il testo originale, intatto». Nessuna allocazione.Cow::Owned(String): «ho dovuto modificarlo, quindi ecco una nuovaString».
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper { Cow::Borrowed(name) // già pulito: restituisci l'originale } else { Cow::Owned(trimmed.to_lowercase()) // va sistemato: crea una nuova String }}
fn main() { for input in ["alice", " Bob ", "CAROL", "dave"] { match normalize(input) { Cow::Borrowed(name) => println!("{input:?} -> {name:?} (in prestito, nessuna copia)"), Cow::Owned(name) => println!("{input:?} -> {name:?} (posseduto, nuova String)"), } }}Output:
"alice" -> "alice" (in prestito, nessuna copia)" Bob " -> "bob" (posseduto, nuova String)"CAROL" -> "carol" (posseduto, nuova String)"dave" -> "dave" (in prestito, nessuna copia)Due nomi su quattro non costano proprio niente. In un programma reale, dove il 99% dell’input è già pulito, sono tantissime copie che non fai mai. Il '_ in Cow<'_, str> dice solo «questo può prendere in prestito dall’input». Se i lifetime ti sembrano ancora confusi, la pagina sui lifetime li spiega.
Usare un Cow come una stringa normale
Sezione intitolata “Usare un Cow come una stringa normale”Di solito non serve affatto fare match su un Cow. Fa deref verso &str, quindi puoi chiamarci sopra i metodi delle stringhe e passare &name a qualsiasi funzione che accetta &str. Quando ti serve davvero una String tutta tua, chiama .into_owned(). Sposta fuori la String se c’è, e fa una copia solo se il Cow era in prestito.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) { Cow::Borrowed(name) } else { Cow::Owned(trimmed.to_lowercase()) }}
fn greet(name: &str) { println!("Benvenuta, {name}!");}
fn main() { let name = normalize(" Eve ");
println!("lunghezza: {}", name.len()); // i metodi di str funzionano direttamente println!("inizia con e: {}", name.starts_with('e')); greet(&name); // &Cow<str> diventa &str
let saved: String = name.into_owned(); // ora è una semplice String println!("{saved:?} salvato nella lista utenti");}Output:
lunghezza: 3inizia con e: trueBenvenuta, eve!"eve" salvato nella lista utentiCow funziona anche con altri tipi, come Cow<'_, [i32]> per una slice che potrebbe dover cambiare. Per il testo, però, quello che vedrai più spesso è Cow<'_, str>.
Quando usare cosa
Sezione intitolata “Quando usare cosa”Ecco un breve promemoria per tutti i tipi «da puntatore» che hai incontrato finora:
| Tipo | Cosa ti dà | Usalo quando |
|---|---|---|
Box<T> |
Un proprietario, valore nell’heap | Tipi ricorsivi, o un valore grande che vuoi spostare a basso costo |
Rc<T> |
Più proprietari, un thread | Più parti del programma condividono gli stessi dati |
Arc<T> |
Più proprietari, tra thread | Come Rc, ma condiviso tra thread (vedi Stato condiviso) |
Weak<T> |
Un collegamento che non possiede | Link al genitore, liste di observer, cache, spezzare i cicli di Rc |
RefCell<T> |
Modifica attraverso un riferimento condiviso, controllata durante l’esecuzione | Devi cambiare qualcosa che vive dentro un Rc |
Cow<'_, T> |
In prestito o posseduto, deciso durante l’esecuzione | Una funzione che di solito restituisce il suo input invariato |
Se non sei sicuro, parti dalla normale ownership e dal prestito. Aggiungi questi strumenti solo quando il compilatore, o un leak di memoria, ti dice che ne hai bisogno.
Errori comuni
Sezione intitolata “Errori comuni”Fare unwrap di un upgrade quando il valore non c’è più
Sezione intitolata “Fare unwrap di un upgrade quando il valore non c’è più”upgrade() restituisce un Option per un motivo. Se fai .unwrap() per abitudine, il programma va in panic nel momento in cui il valore è stato eliminato:
use std::rc::Rc;
fn main() { let session = Rc::new(String::from("user-42")); let link = Rc::downgrade(&session);
drop(session); // la sessione finisce
let current = link.upgrade().unwrap(); println!("ancora connesso come {current}");}Quando lo esegui, ottieni:
thread 'main' (12345) panicked at src/main.rs:9:34:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceIl messaggio di panic (in inglese, come lo stampa Rust) dice che è stato chiamato unwrap() su un valore None. La soluzione è trattare «non c’è più» come un caso normale, con match o if let:
match link.upgrade() { Some(current) => println!("ancora connesso come {current}"), None => println!("la sessione è finita"),}Usare Rc per il collegamento all’indietro
Sezione intitolata “Usare Rc per il collegamento all’indietro”Se un figlio tiene un Rc<Parent> e il genitore tiene un Rc<Child>, hai costruito il ciclo della prima sezione. Non va in crash niente e niente ti avvisa; la memoria semplicemente non torna più. Una buona regola: decidi chi possiede chi, usa Rc in quella direzione e Weak per ogni collegamento che va nell’altra.
struct Folder { parent: RefCell<Rc<Folder>>, // sbagliato: il figlio tiene in vita il genitore children: RefCell<Vec<Rc<Folder>>>,}Cambia parent in RefCell<Weak<Folder>> e il ciclo sparisce.
Pratica
Sezione intitolata “Pratica”1. Commenti su un post eliminato
Sezione intitolata “1. Commenti su un post eliminato”Crea un Post con un title, tenuto in un Rc. Crea un Comment con un text e un post: Weak<Post>. Scrivi una funzione describe(comment: &Comment) che stampi "<testo>" su <titolo> se il post esiste ancora, oppure "<testo>" su un post eliminato se non esiste più. Chiamala, elimina il post e chiamala di nuovo. Suggerimento: usa match comment.post.upgrade().
Mostra la soluzione
use std::rc::{Rc, Weak};
struct Post { title: String,}
struct Comment { text: String, post: Weak<Post>,}
fn describe(comment: &Comment) { match comment.post.upgrade() { Some(post) => println!("\"{}\" su {}", comment.text, post.title), None => println!("\"{}\" su un post eliminato", comment.text), }}
fn main() { let post = Rc::new(Post { title: String::from("Imparare Rust") }); let comment = Comment { text: String::from("Ottimo post!"), post: Rc::downgrade(&post) };
describe(&comment); drop(post); describe(&comment);}Output:
"Ottimo post!" su Imparare Rust"Ottimo post!" su un post eliminatoIl commento non tiene mai in vita il post. Appena l’unico Rc viene eliminato, upgrade() restituisce None e il commento gestisce la cosa con calma invece di andare in panic.
2. Fare l’escape del testo solo quando serve
Sezione intitolata “2. Fare l’escape del testo solo quando serve”Scrivi fn escape(text: &str) -> Cow<'_, str> che sostituisce ogni < con <. Se il testo non contiene <, restituiscilo in prestito. Provala su "ciao mondo" e "1 < 2", e stampa quale variante hai ottenuto. Suggerimento: controlla prima con text.contains('<'), poi usa text.replace('<', "<") per il caso posseduto.
Mostra la soluzione
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } else { Cow::Borrowed(text) }}
fn main() { for text in ["ciao mondo", "1 < 2"] { match escape(text) { Cow::Borrowed(s) => println!("{s} (in prestito)"), Cow::Owned(s) => println!("{s} (posseduto)"), } }}Output:
ciao mondo (in prestito)1 < 2 (posseduto)Solo il testo che contiene davvero < paga il costo di una nuova String. Tutto il resto passa dritto, intatto.
Riepilogo
Sezione intitolata “Riepilogo”- Due valori
Rcche puntano l’uno all’altro non arrivano mai a un conteggio forte di 0, quindi la memoria viene persa e il loroDropnon viene mai eseguito. Rc::downgradecrea unWeak<T>, che non tiene in vita il valore.upgrade()restituisce unOption<Rc<T>>.- Negli alberi, i figli si possiedono con
Rce i collegamenti al genitore sonoWeak. L’ownership scorre verso il basso, i collegamenti deboli puntano verso l’alto. - Le liste di observer e le cache possono contenere
Weak, così non tengono mai in vita le cose per sbaglio. Cow<'_, str>restituisce il testo in prestito quando non è cambiato niente, e unaStringposseduta solo quando qualcosa è cambiato. Fa deref verso&str, e.into_owned()ti dà unaString.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Due valori tengono ciascuno un Rc all'altro. Che cosa succede quando main finisce?
2. Che cosa restituisce weak.upgrade()?
3. In un albero di cartelle, quale collegamento dovrebbe essere Weak?
4. Quando una funzione che restituisce Cow<'_, str> evita di creare una nuova String?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Reference Cycles Can Leak Memory — The Rust Book (in inglese): la spiegazione ufficiale dei cicli di Rc e di un albero con collegamenti
Weakal genitore. std::rc::Weak— documentazione della libreria standard (in inglese): tutti i metodi diWeak, compresiupgrade,strong_counteWeak::new.std::rc— documentazione della libreria standard (in inglese): come lavorano insieme i conteggi forti e deboli, con un esempio più lungo su come spezzare i cicli.std::borrow::Cow— documentazione della libreria standard (in inglese): l’enumCow,to_mut,into_owneded esempi che vanno oltre le stringhe.