Ownership in Rust
Lâownership (proprietĂ ) è il concetto piĂš importante e distintivo di Rust. Ă il meccanismo che permette a Rust di garantire la sicurezza della memoria senza garbage collector. Ogni valore in Rust ha un unico proprietario, e quando il proprietario esce dallo scope, il valore viene automaticamente deallocato. Comprendere lâownership è essenziale per scrivere codice Rust corretto.
Le tre regole dellâownership
Lâintero sistema di ownership si basa su tre regole fondamentali:
- Ogni valore in Rust ha una variabile che è il suo proprietario (owner).
- Può esserci un solo proprietario alla volta per ogni valore.
- Quando il proprietario esce dallo scope, il valore viene deallocato (dropped).
fn main() {
{
let s = String::from("ciao"); // s è il proprietario della String
println!("{s}");
} // s esce dallo scope: la memoria viene liberata automaticamente
// s non è piÚ accessibile qui
}
Stack vs Heap
Per capire lâownership, è importante distinguere tra stack e heap:
- Stack: memoria veloce, dimensione fissa e nota a compile time. Qui vivono interi, float, bool, char, tuple di tipi Copy e riferimenti.
- Heap: memoria dinamica, per dati la cui dimensione non è nota a compile time. Qui vivono
String,Vec<T>,Box<T>e altri tipi allocati dinamicamente.
fn main() {
let x = 42; // Stack: intero a dimensione fissa
let y = true; // Stack: booleano
let s = String::from("hello"); // Stack: puntatore, lunghezza, capacitĂ
// Heap: i dati effettivi "hello"
}
Lâownership gestisce principalmente i dati sullâheap, perche sono quelli che necessitano di una deallocazione esplicita.
Move semantics
Quando assegni un valore heap a unâaltra variabile, Rust non copia il dato: lo muove. Il proprietario originale non è piĂš valido.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 viene "moved" in s2
// println!("{s1}"); // Errore! s1 non è piÚ valido
println!("{s2}"); // OK: s2 è il nuovo proprietario
}
Questo comportamento previene il double free: se entrambe le variabili fossero valide, la memoria verrebbe deallocata due volte quando escono dallo scope.
Il move avviene anche nel passaggio a funzioni:
fn stampa(s: String) {
println!("{s}");
} // s viene deallocata qui
fn main() {
let messaggio = String::from("Ciao Rust");
stampa(messaggio);
// println!("{messaggio}"); // Errore! messaggio è stato moved
}
Il trait Copy
I tipi che vivono interamente sullo stack implementano il trait Copy. Per questi tipi, lâassegnamento crea una copia completa anzichĂŠ un move, e il valore originale resta valido.
fn main() {
let x = 42;
let y = x; // Copia, non move!
println!("x = {x}, y = {y}"); // Entrambi validi
let a = 3.14;
let b = a; // Copia
let c = true;
let d = c; // Copia
let e = 'A';
let f = e; // Copia
println!("{a} {b} {c} {d} {e} {f}"); // Tutto valido
}
Tipi che implementano Copy: tutti gli interi (i8, i32, u64, ecc.), i float (f32, f64), bool, char, tuple composte solo da tipi Copy, e i riferimenti &T.
Clone per copie profonde
Per i tipi che non implementano Copy, si può usare .clone() per creare una copia profonda esplicita. Questo alloca nuova memoria sullâheap.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Copia profonda esplicita
println!("s1 = {s1}"); // Valido
println!("s2 = {s2}"); // Valido, è una copia indipendente
}
clone() è unâoperazione potenzialmente costosa perchĂŠ duplica i dati sullâheap. Usala consapevolmente.
fn main() {
let nomi = vec![String::from("Alice"), String::from("Bob")];
let nomi_copia = nomi.clone(); // Clona il vettore e tutte le stringhe
println!("Originale: {:?}", nomi);
println!("Copia: {:?}", nomi_copia);
}
Ownership e funzioni
Il passaggio di valori a funzioni segue le stesse regole dellâassegnamento: i tipi Copy vengono copiati, gli altri vengono moved.
fn prende_ownership(s: String) {
println!("Ho ricevuto: {s}");
} // s viene deallocata
fn crea_copia(n: i32) {
println!("Ho ricevuto una copia: {n}");
} // n è solo una copia, l'originale resta valido
fn main() {
let stringa = String::from("hello");
let numero = 42;
prende_ownership(stringa);
// stringa non è piÚ valida
crea_copia(numero);
println!("numero è ancora: {numero}"); // OK: è stato copiato
}
Valori di ritorno e ownership
Le funzioni possono restituire lâownership di un valore al chiamante.
fn crea_stringa() -> String {
let s = String::from("creata nella funzione");
s // L'ownership viene trasferita al chiamante
}
fn prendi_e_restituisci(s: String) -> String {
println!("Usando: {s}");
s // Restituisce l'ownership
}
fn main() {
let s1 = crea_stringa();
println!("{s1}");
let s2 = String::from("hello");
let s3 = prendi_e_restituisci(s2);
// s2 non è piÚ valido, ma s3 lo è
println!("{s3}");
}
Questo pattern di âprendi e restituisciâ è scomodo. Per questo Rust offre il borrowing (prestito tramite riferimenti), che permette di usare un valore senza prenderne lâownership.
Perche lâownership è importante
Lâownership risolve diversi problemi di sicurezza della memoria presenti in altri linguaggi:
- Niente double free: un valore ha un solo proprietario, quindi viene deallocato una sola volta.
- Niente use-after-free: una volta che il proprietario esce dallo scope, il compilatore impedisce lâaccesso al valore.
- Niente dangling pointer: il compilatore garantisce che i riferimenti siano sempre validi.
- Niente data race: le regole di borrowing impediscono accessi concorrenti non sicuri.
- Niente garbage collector: la memoria viene gestita in modo deterministico a costo zero a runtime.
fn main() {
let mut dati = vec![1, 2, 3];
// Il compilatore impedisce situazioni pericolose:
// let riferimento = &dati[0];
// dati.push(4); // Errore! Non si può modificare dati mentre esiste un riferimento
dati.push(4); // OK: nessun riferimento attivo
println!("{:?}", dati);
}
Conclusione
Lâownership è il cuore di Rust e la ragione principale per cui il linguaggio può garantire sicurezza della memoria senza costi a runtime. Le tre regole sono semplici da enunciare ma hanno implicazioni profonde. Padroneggiare lâownership, insieme al borrowing e ai lifetime, è la chiave per scrivere codice Rust idiomatico. Inizialmente il borrow checker può sembrare severo, ma col tempo si apprezza come una guida che spinge verso design migliori.