00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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:

  1. Ogni valore in Rust ha una variabile che è il suo proprietario (owner).
  2. Può esserci un solo proprietario alla volta per ogni valore.
  3. 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:

  1. Niente double free: un valore ha un solo proprietario, quindi viene deallocato una sola volta.
  2. Niente use-after-free: una volta che il proprietario esce dallo scope, il compilatore impedisce l’accesso al valore.
  3. Niente dangling pointer: il compilatore garantisce che i riferimenti siano sempre validi.
  4. Niente data race: le regole di borrowing impediscono accessi concorrenti non sicuri.
  5. 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.