Salta ai contenuti

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: 2
strong count di bob: 2
fine di main
▶ Provalo tu

Nota 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.

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 è bob
alice: strong = 1, weak = 1
fine di main
elimino bob
elimino alice
▶ Provalo tu

Questa 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 sempre None. È un comodo valore di partenza.
  • Rc::strong_count conta i proprietari. Rc::weak_count conta i collegamenti deboli. Solo il conteggio forte decide quando il valore viene eliminato.
  • L’Rc che ottieni da upgrade() è 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 casa
percorso completo: casa/foto/viaggi
casa: strong = 1, weak = 1
foto: strong = 2, weak = 1
▶ Provalo tu

Leggi 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.

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 10
logger ha ricevuto: punteggio 10
schermo ha ricevuto: punteggio 20
(un listener non c'è più, lo salto)
listener rimasti: 1
▶ Provalo tu

La 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 nuova String».
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)
▶ Provalo tu

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.

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: 3
inizia con e: true
Benvenuta, eve!
"eve" salvato nella lista utenti
▶ Provalo tu

Cow 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>.

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.

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` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Provalo tu

Il 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"),
}

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.

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 eliminato
▶ Provalo tu

Il 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.

Scrivi fn escape(text: &str) -> Cow<'_, str> che sostituisce ogni < con &lt;. 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('<', "&lt;") per il caso posseduto.

Mostra la soluzione
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} 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 &lt; 2 (posseduto)
▶ Provalo tu

Solo il testo che contiene davvero < paga il costo di una nuova String. Tutto il resto passa dritto, intatto.

  • Due valori Rc che puntano l’uno all’altro non arrivano mai a un conteggio forte di 0, quindi la memoria viene persa e il loro Drop non viene mai eseguito.
  • Rc::downgrade crea un Weak<T>, che non tiene in vita il valore. upgrade() restituisce un Option<Rc<T>>.
  • Negli alberi, i figli si possiedono con Rc e i collegamenti al genitore sono Weak. 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 una String posseduta solo quando qualcosa è cambiato. Fa deref verso &str, e .into_owned() ti dà una String.

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