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

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.