saverioriotto.it

Ownership, Borrowing e Lifetimes: Come Rust Gestisce la Memoria

Scopri come Rust garantisce la sicurezza della memoria con Ownership, Borrowing e Lifetimes. Una guida completa con esempi pratici per padroneggiare questi concetti fondamentali.

Ownership, Borrowing e Lifetimes: Come Rust Gestisce la Memoria

Rust è famoso per il suo sistema di gestione della memoria basato su concetti come ownership, borrowing e lifetimes, che consentono di scrivere codice sicuro senza garbage collector. Questi concetti possono sembrare complessi all’inizio, ma sono essenziali per capire come Rust previene errori comuni come use-after-free o data races nei programmi concorrenti.

In questa lezione, esploreremo come funzionano ownership, borrowing e lifetimes, con esempi pratici per consolidare la comprensione.

Ownership: Il Cuore di Rust

In Rust, ogni valore ha un proprietario (owner), e ci sono regole precise che determinano come avviene il trasferimento di ownership:

  1. Ogni valore ha un solo proprietario alla volta.
  2. Quando il proprietario esce dallo scope, il valore viene automaticamente liberato.

Esempio: Ownership di Base

fn main() {
    let s1 = String::from("Ciao");
    let s2 = s1; // Ownership trasferita a s2
    // println!("{}", s1); // Errore: s1 non è più valido
    println!("{}", s2);
}

In questo caso, quando assegniamo s1 a s2, l’ownership di s1 viene trasferita a s2. Pertanto, s1 non è più valido.

Movimento dei Dati

Il movimento dell’ownership evita duplicati non sicuri di dati. Tuttavia, se vuoi duplicare un valore, puoi usare il metodo clone:

fn main() {
    let s1 = String::from("Ciao");
    let s2 = s1.clone(); // Clonazione esplicita
    println!("{}", s1); // Ora s1 è ancora valido
    println!("{}", s2);
}

Borrowing: Prendere in Prestito senza Perdere l'Ownership

Per evitare di trasferire l’ownership, Rust permette di prendere in prestito un valore, sia in modo immutabile che mutabile.

Borrowing Immutabile

Un riferimento immutabile (&T) consente di leggere i dati senza modificarli.

fn stampa(s: &String) {
    println!("{}", s);
}

fn main() {
    let s1 = String::from("Ciao");
    stampa(&s1); // Prende in prestito s1
    println!("{}", s1); // s1 è ancora valido
}

Puoi avere più riferimenti immutabili contemporaneamente:

fn main() {
    let s = String::from("Ciao");
    let r1 = &s;
    let r2 = &s;
    println!("{} e {}", r1, r2);
}

Borrowing Mutabile

Un riferimento mutabile (&mut T) permette di modificare i dati, ma ci sono restrizioni:

  1. Può esistere un solo riferimento mutabile alla volta.
  2. Non puoi avere contemporaneamente riferimenti immutabili e mutabili allo stesso valore.
fn aggiungi(s: &mut String) {
    s.push_str(", mondo!");
}

fn main() {
    let mut s1 = String::from("Ciao");
    aggiungi(&mut s1); // Prende in prestito in modo mutabile
    println!("{}", s1);
}

Lifetimes: Durata dei Riferimenti

I lifetimes sono un meccanismo che assicura che i riferimenti siano sempre validi. Rust utilizza le lifetimes per evitare errori di memoria come i riferimenti pendenti (dangling references).

Esempio di Riferimento Non Valido

fn crea_stringa() -> &String {
    let s = String::from("Ciao");
    &s // Errore: s esce dallo scope
}

In questo esempio, la stringa s viene creata all’interno della funzione e viene liberata appena la funzione termina, lasciando un riferimento non valido.

Annotazioni di Lifetime

Puoi utilizzare annotazioni di lifetime per specificare quanto a lungo un riferimento è valido. Un esempio comune è con i parametri di una funzione:

fn maggiore<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let str1 = String::from("Ciao");
    let str2 = String::from("Rust");
    let risultato = maggiore(&str1, &str2);
    println!("La stringa più lunga è: {}", risultato);
}

Qui, l’annotazione 'a indica che il riferimento restituito ha la stessa durata dei parametri.

Esercizi Pratici

  1. Esercizio 1: Scrivi una funzione che accetta un riferimento immutabile a un vettore di numeri interi e restituisce il valore massimo.
  2. Esercizio 2: Implementa una funzione che modifica una stringa passata tramite un riferimento mutabile.
  3. Esercizio 3: Usa i lifetimes per confrontare due stringhe e restituire quella con il numero maggiore di caratteri.
  4. Esercizio 4: Prova a creare un riferimento non valido e osserva come il compilatore di Rust previene l’errore.

Conclusione

I concetti di ownership, borrowing e lifetimes sono il fondamento della gestione sicura della memoria in Rust. Anche se inizialmente possono sembrare complessi, padroneggiarli ti aiuterà a scrivere codice robusto e privo di errori legati alla memoria. Nel prossimo articolo esporeremo concetti come Strutture, Enum e Pattern Matching. Continua a esercitarti con gli esempi forniti e vedrai che questi concetti diventeranno naturali nel tuo sviluppo con Rust!




Commenti
* Obbligatorio