Salta al contenuto
Rust

Borrowing e Riferimenti in Rust

Il borrowing (prestito) è il meccanismo che permette di accedere ai dati senza prenderne l’ownership. Invece di trasferire la proprietà, si crea un riferimento al dato. Il borrow checker di Rust verifica a compile time che tutti i riferimenti siano validi, prevenendo data race, dangling pointer e use-after-free senza alcun costo a runtime.

Riferimenti immutabili (&T)

Un riferimento immutabile &T permette di leggere un dato senza modificarlo e senza prenderne l’ownership.

fn calcola_lunghezza(s: &String) -> usize {
    s.len()
}

fn main() {
    let nome = String::from("Rust");
    let lunghezza = calcola_lunghezza(&nome);

    // nome è ancora valido perché abbiamo solo prestato un riferimento
    println!("'{}' ha {} caratteri", nome, lunghezza);
}

Il simbolo & crea un riferimento (nel chiamante) e indica un parametro per riferimento (nella firma della funzione). Si possono avere quanti riferimenti immutabili si vuole contemporaneamente.

fn main() {
    let dato = String::from("hello");

    let r1 = &dato;
    let r2 = &dato;
    let r3 = &dato;

    println!("{r1}, {r2}, {r3}"); // Tutto valido
}

Riferimenti mutabili (&mut T)

Un riferimento mutabile &mut T permette di leggere e modificare il dato prestato.

fn aggiungi_punto(s: &mut String) {
    s.push('.');
}

fn main() {
    let mut testo = String::from("Ciao");
    aggiungi_punto(&mut testo);
    println!("{testo}"); // Ciao.
}

Per creare un riferimento mutabile, sia la variabile che il riferimento devono essere mutabili: la variabile con let mut e il riferimento con &mut.

fn raddoppia(n: &mut i32) {
    *n *= 2; // Dereferenzia e modifica
}

fn main() {
    let mut valore = 21;
    raddoppia(&mut valore);
    println!("{valore}"); // 42
}

Le regole fondamentali

Il borrow checker applica due regole cruciali:

  1. Puoi avere UN riferimento mutabile OPPURE quanti riferimenti immutabili vuoi, ma non entrambi contemporaneamente.
  2. I riferimenti devono essere sempre validi (niente dangling reference).

Regola 1: EsclusivitĂ  dei riferimenti mutabili

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    // let r2 = &mut s; // Errore! Non si possono avere due &mut contemporanei

    println!("{r1}");
}

Non è possibile nemmeno avere un riferimento immutabile e uno mutabile allo stesso tempo:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;      // OK: riferimento immutabile
    let r2 = &s;      // OK: altro riferimento immutabile
    // let r3 = &mut s; // Errore! Non si può avere &mut mentre esistono &

    println!("{r1}, {r2}");
}

Questa regola previene i data race a compile time.

Regola 2: Niente riferimenti pendenti (dangling)

Rust impedisce di creare riferimenti a dati che non esistono piĂą.

// Questo NON compila:
// fn crea_riferimento() -> &String {
//     let s = String::from("hello");
//     &s // Errore! s viene deallocata alla fine della funzione
// }

// Soluzione: restituisci l'ownership
fn crea_stringa() -> String {
    let s = String::from("hello");
    s // Trasferisci l'ownership al chiamante
}

fn main() {
    let s = crea_stringa();
    println!("{s}");
}

Il Borrow Checker

Il borrow checker è il componente del compilatore Rust che verifica il rispetto delle regole di borrowing. Analizza il codice a compile time e rifiuta programmi che violerebbero la sicurezza della memoria.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let primo = &v[0]; // Riferimento immutabile a un elemento

    // v.push(6); // Errore! push potrebbe riallocare il vettore,
                   // invalidando il riferimento 'primo'

    println!("Il primo elemento è: {primo}");

    // Dopo l'ultimo uso di 'primo', possiamo modificare v
    v.push(6); // OK: nessun riferimento immutabile attivo
    println!("{:?}", v);
}

Non-Lexical Lifetimes (NLL)

A partire dalla Rust Edition 2018, il compilatore usa i Non-Lexical Lifetimes: lo scope di un riferimento termina all’ultimo punto in cui viene effettivamente usato, non alla fine del blocco. Questo rende il borrow checker piu permissivo.

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{r1} e {r2}");
    // r1 e r2 non vengono piĂą usati dopo questa riga

    // Ora possiamo creare un riferimento mutabile
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{r3}");
}

Prima di NLL, questo codice non sarebbe compilato perche r1 e r2 sarebbero rimasti “vivi” fino alla fine del blocco.

Pattern comuni con i riferimenti

Funzioni che prendono in prestito

fn contiene(testo: &str, parola: &str) -> bool {
    testo.contains(parola)
}

fn primo_e_ultimo(v: &[i32]) -> Option<(i32, i32)> {
    if v.is_empty() {
        None
    } else {
        Some((v[0], v[v.len() - 1]))
    }
}

fn main() {
    let frase = String::from("Rust è fantastico");
    println!("Contiene 'Rust': {}", contiene(&frase, "Rust"));

    let numeri = vec![10, 20, 30, 40];
    if let Some((primo, ultimo)) = primo_e_ultimo(&numeri) {
        println!("Primo: {primo}, Ultimo: {ultimo}");
    }
}

Modificare attraverso riferimenti

fn ordina_e_deduplica(v: &mut Vec<i32>) {
    v.sort();
    v.dedup();
}

fn main() {
    let mut dati = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
    println!("Prima: {:?}", dati);

    ordina_e_deduplica(&mut dati);
    println!("Dopo: {:?}", dati);
}

Iterazione con riferimenti

fn main() {
    let nomi = vec![String::from("Alice"), String::from("Bob"), String::from("Carlo")];

    // Itera con riferimenti immutabili
    for nome in &nomi {
        println!("Ciao, {nome}!");
    }

    // nomi è ancora utilizzabile
    println!("Totale: {} nomi", nomi.len());
}
Tipo Sintassi Può leggere Può modificare Quanti alla volta
Ownership T Si Si 1 (proprietario)
Rif. immutabile &T Si No Illimitati
Rif. mutabile &mut T Si Si Esattamente 1

Conclusione

Il borrowing è il complemento naturale dell’ownership: dove l’ownership trasferisce la proprietà, il borrowing la presta temporaneamente. Le regole possono sembrare restrittive all’inizio, ma prevengono intere categorie di bug a compile time. Il borrow checker, con i Non-Lexical Lifetimes, è sufficientemente intelligente da permettere la maggior parte dei pattern di utilizzo naturali. Quando il borrow checker rifiuta il tuo codice, spesso sta segnalando un vero problema di design.

Contenuto redatto con l'assistenza di strumenti di intelligenza artificiale e rivisto dalla redazione di Codegrind, che ne è responsabile. Hai trovato un errore? Scrivici.