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:
- Puoi avere UN riferimento mutabile OPPURE quanti riferimenti immutabili vuoi, ma non entrambi contemporaneamente.
- 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());
}
Riepilogo
| 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.