Ownership in Rust
L’ownership (possesso) è l’idea che rende Rust Rust. È il modo in cui il linguaggio garantisce la sicurezza della memoria senza un garbage collector e senza che tu debba liberare la memoria a mano. All’inizio sembra strana, ma il concetto di base è semplice. Costruiamolo un passo alla volta.
Il problema che l’ownership risolve
Sezione intitolata “Il problema che l’ownership risolve”Quando un programma gira, salva i dati in memoria. Prima o poi quella memoria va ripulita, altrimenti il programma perde memoria (memory leak) e alla fine si blocca. I linguaggi affrontano il problema in due modi comuni:
- A mano (C/C++): sei tu a liberare la memoria. Se te ne dimentichi, hai un leak. Se la liberi troppo presto, ottieni crash e falle di sicurezza.
- Garbage collector (Java, Go, Python): un processo in background fa pulizia al posto tuo. Comodo, ma costa in prestazioni e prevedibilità.
Rust sceglie una terza strada: decide in fase di compilazione esattamente quando ogni valore va ripulito, seguendo alcune regole sull’ownership. Niente memoria da liberare a mano, niente garbage collector.
Le tre regole dell’ownership
Sezione intitolata “Le tre regole dell’ownership”Questo è tutto il sistema. Il resto viene di conseguenza:
- Ogni valore ha un solo proprietario (una variabile che lo «possiede»).
- Può esserci un solo proprietario alla volta.
- Quando il proprietario esce dal suo scope (ambito), il valore viene ripulito automaticamente.
Vediamo ogni regola in azione.
Regola 3: i valori vengono rilasciati alla fine dello scope
Sezione intitolata “Regola 3: i valori vengono rilasciati alla fine dello scope”Uno scope è la zona tra { e }. Quando lo scope di una variabile finisce, Rust libera automaticamente il suo valore: non scrivi mai free().
fn main() { { let message = String::from("ciao"); // qui message è valida println!("{message}"); } // <- lo scope finisce, `message` viene ripulita automaticamente
// println!("{message}"); // ❌ errore: qui message non esiste più}Non hai dovuto liberare niente: Rust l’ha fatto nel momento in cui message è uscita dallo scope.
Regole 1 e 2: lo spostamento (move)
Sezione intitolata “Regole 1 e 2: lo spostamento (move)”Qui le cose si fanno interessanti. Guarda cosa succede quando assegni una variabile a un’altra:
fn main() { let s1 = String::from("ciao"); let s2 = s1; // l'ownership SI SPOSTA da s1 a s2
println!("{s2}"); // ✅ funziona // println!("{s1}"); // ❌ errore: s1 è stata spostata}Quando scrivi let s2 = s1, Rust non copia la stringa: sposta l’ownership da s1 a s2. Siccome la regola 2 dice che può esserci un solo proprietario, ora s1 non è più valida. Se provi a usarla, ottieni:
error[E0382]: borrow of moved value: `s1`Il compilatore ti dice, in inglese, che stai usando (prendendo in prestito) un valore già spostato.
E se voglio due copie?
Sezione intitolata “E se voglio due copie?”Se ti serve davvero una seconda copia indipendente, chiama .clone():
fn main() { let s1 = String::from("ciao"); let s2 = s1.clone(); // fa una copia completa
println!("s1 = {s1}, s2 = {s2}"); // ✅ ora funzionano entrambe}clone() è esplicito apposta: copiare può costare caro, quindi Rust vuole che tu lo chieda.
I tipi semplici vengono copiati, non spostati
Sezione intitolata “I tipi semplici vengono copiati, non spostati”Forse ti chiedi perché questo codice non dà errore:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ funzionano entrambe}I valori piccoli e di dimensione fissa, come interi, numeri in virgola mobile, booleani e char, costano pochissimo da copiare, quindi Rust li copia invece di spostarli. Da principiante, la regola pratica è:
- Valori semplici (numeri,
bool,char) → copiati. L’originale resta valido. - Dati posseduti (come
String) → spostati. L’originale non è più valido.
Ownership e funzioni
Sezione intitolata “Ownership e funzioni”Anche passare un valore a una funzione lo sposta, proprio come assegnarlo a una variabile:
fn take_ownership(s: String) { println!("Ora possiedo: {s}");} // qui s viene rilasciata
fn main() { let text = String::from("ciao"); take_ownership(text); // println!("{text}"); // ❌ errore: text è stata spostata nella funzione}È proprio per questo che esiste il prossimo argomento, il prestito (borrowing). Il prestito permette a una funzione di usare un valore senza prenderne l’ownership, così puoi continuare a usarlo dopo.
Pratica
Sezione intitolata “Pratica”Prova prima da solo ogni esercizio, nel Rust Playground o in un progetto locale. Gli errori di compilazione fanno parte del gioco: leggili, di solito indicano proprio lo spostamento.
1. Tieni l’originale
Sezione intitolata “1. Tieni l’originale”Questo programma non compila. Correggilo in modo che stampi bozza -> bozza v2, senza togliere nessuna delle due variabili.
fn main() { let original = String::from("bozza"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("bozza"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveSuggerimento: ti servono due stringhe indipendenti, così modificarne una non tocca l’altra.
Mostra la soluzione
fn main() { let original = String::from("bozza"); let mut edited = original.clone(); // una copia completa e separata edited.push_str(" v2"); println!("{original} -> {edited}");}Output:
bozza -> bozza v2let mut edited = original; sposta la stringa, quindi original non è più valida. .clone() crea una seconda stringa con la sua memoria. Ora push_str modifica solo edited, e original dice ancora bozza.
2. Restituiscila
Sezione intitolata “2. Restituiscila”Scrivi una funzione add_signature che prende l’ownership di una String, aggiunge " -- Ferris" alla fine e restituisce la stringa. Chiamala da main e stampa il risultato.
Suggerimento: una funzione può restituire il valore che ha ricevuto. Scrivi -> String nella firma e prendi il parametro come mut text: String, così hai il permesso di modificarlo.
Mostra la soluzione
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // l'ownership torna fuori, al chiamante}
fn main() { let letter = String::from("A presto!"); let letter = add_signature(letter); println!("{letter}");}Output:
A presto! -- FerrisL’ownership entra nella funzione con la chiamata ed esce di nuovo con il valore di ritorno. La raccogliamo in una nuova variabile letter (che oscura la vecchia, cioè fa shadowing). Funziona, ma passare valori avanti e indietro di continuo diventa faticoso. È proprio il problema che risolve il prestito.
3. Di chi è adesso?
Sezione intitolata “3. Di chi è adesso?”Questo programma non compila. Correggilo in modo che stampi Ana è entrata nella lista e poi ["Ana"].
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name} è entrata nella lista"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} è entrata nella lista"); | ^^^^ value borrowed here after moveSuggerimento: push prende l’ownership, proprio come una chiamata di funzione. Chi possiede la stringa dopo la riga 4? Usa name prima dello spostamento, oppure chiedila al nuovo proprietario.
Mostra la soluzione
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name} è entrata nella lista"); // usala finché è ancora nostra names.push(name); // ora il proprietario è il vettore println!("{:?}", names);}Output:
Ana è entrata nella lista["Ana"]Dopo names.push(name), il proprietario è il vettore e name non si può più usare. Stampare prima è una soluzione. Un’altra è tenere l’ordine originale e stampare names[0], perché il vettore ha ancora la stringa. Anche clonare funzionerebbe, ma qui non serve copiare niente.
Riepilogo
Sezione intitolata “Riepilogo”- L’ownership permette a Rust di gestire la memoria in modo sicuro, senza garbage collector.
- Tre regole: un proprietario per valore, un proprietario alla volta, il valore viene rilasciato quando il proprietario esce dallo scope.
- Assegnare dati posseduti (come una
String) li sposta: l’originale non è più valido. - Usa
.clone()per una copia indipendente; i tipi semplici (numeri,bool,char) vengono copiati automaticamente. - Anche passare un valore a una funzione lo sposta, e questo ci porta al prestito.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quanti proprietari può avere un valore nello stesso momento, in Rust?
2. Quale metodo chiami per fare una copia indipendente di una String invece di spostarla?
3. Quale di questi tipi viene copiato automaticamente, invece che spostato, quando lo assegni a un'altra variabile?
4. Cosa succede quando passi una String a una funzione che la prende per valore (non per riferimento)?
5. Secondo questa pagina, quando Rust ripulisce (rilascia) automaticamente un valore?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”- What Is Ownership? — The Rust Book (in inglese): il capitolo ufficiale, con disegni di come appare una
Stringin memoria e del perché uno spostamento non la copia. - Ownership and moves — Rust by Example (in inglese): esempi brevi ed eseguibili di spostamenti, anche dentro le funzioni.
- RAII — Rust by Example (in inglese): come vengono ripuliti i valori quando escono dallo scope, e che ruolo ha il trait
Drop. Copy— documentazione della libreria standard (in inglese): quali tipi vengono copiati invece che spostati, e la differenza traCopyeClone.- Rust track — Exercism (in inglese): esercizi gratuiti con feedback, per quando vuoi più pratica di quella che trovi in questa pagina.