Prestito (borrowing) in Rust
Nella pagina sull’ownership hai visto un problema: passare una String a una funzione la sposta, quindi dopo non puoi più usarla. Spostare valori di continuo (o clonarli) sarebbe scomodo. Il prestito (borrowing) è la soluzione di Rust.
Prestare significa lasciare che un pezzo di codice usi un valore senza prenderne l’ownership, come quando presti un libro a un amico. Lo legge, poi te lo restituisce, e il libro resta tuo.
Riferimenti: prestare con &
Sezione intitolata “Riferimenti: prestare con &”Crei un riferimento mettendo & davanti a un valore. Un riferimento permette a una funzione di guardare i dati senza possederli:
fn print_length(s: &String) { // prende in prestito s, non la possiede println!("Lunghezza: {}", s.len());}
fn main() { let text = String::from("ciao"); print_length(&text); // presta text alla funzione println!("Ancora mia: {text}"); // ✅ qui text è ancora valida}Output:
Lunghezza: 4Ancora mia: ciao&text vuol dire «lascia che questa funzione prenda in prestito text». Siccome la funzione l’ha solo presa in prestito, l’ownership resta a main e puoi continuare a usare text anche dopo. Confrontalo con la pagina sull’ownership, dove lo stesso codice senza & dava errore.
Riferimenti mutabili: prestare per modificare
Sezione intitolata “Riferimenti mutabili: prestare per modificare”Un semplice riferimento & è di sola lettura: puoi guardare ma non toccare. Se una funzione deve modificare il valore che ha in prestito, usa un riferimento mutabile con &mut:
fn add_excitement(s: &mut String) { s.push_str("!!!");}
fn main() { let mut text = String::from("ciao"); add_excitement(&mut text); // prestala in modo mutabile println!("{text}"); // ciao!!!}Output:
ciao!!!Nota che tre cose devono combaciare: text è dichiarata mut, la funzione chiede un &mut String e la chiamata passa &mut text. Servono tutte e tre per modificare un valore preso in prestito.
Le regole del prestito
Sezione intitolata “Le regole del prestito”Rust impone due regole sui riferimenti. Esistono per evitare le data race, un’intera famiglia di bug in cui due parti del codice toccano gli stessi dati nello stesso momento e li corrompono.
In ogni momento puoi avere o:
- un numero qualsiasi di riferimenti di sola lettura (
&), oppure - esattamente un riferimento mutabile (
&mut),
ma mai entrambi insieme.
In parole semplici: tanti lettori oppure un solo scrittore, mai le due cose insieme. Questo codice viola la regola:
fn main() { let mut text = String::from("hello");
let r1 = &text; // a read-only borrow let r2 = &mut text; // ❌ can't borrow mutably while r1 exists
println!("{r1} {r2}");}Il compilatore ti ferma:
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutableIl messaggio (in inglese) dice che non puoi prendere text in prestito come mutabile, perché è già in prestito come immutabile tramite r1.
Quando finisce un prestito?
Sezione intitolata “Quando finisce un prestito?”Potresti aspettarti che un prestito duri fino alla } che chiude il suo scope. Non è così. Un prestito dura solo fino all’ultimo punto in cui il riferimento viene usato. Dopo, il valore si può prendere di nuovo in prestito, anche in modo mutabile.
Quindi l’esempio sbagliato qui sopra si corregge semplicemente finendo di usare r1 prima di creare r2:
fn main() { let mut text = String::from("ciao");
let r1 = &text; println!("Leggo: {r1}"); // ultimo uso di r1: il suo prestito finisce qui
let r2 = &mut text; // ✅ va bene: nessuno sta più leggendo r2.push_str(", mondo");
println!("{text}");}Output:
Leggo: ciaociao, mondoLa regola è sempre «tanti lettori oppure un solo scrittore». Viene solo controllata sul tratto di codice in cui ogni riferimento è davvero usato. Il lettore r1 ha finito quando compare lo scrittore r2, e r2 ha finito prima che l’ultimo println! legga di nuovo text. Niente si sovrappone, quindi Rust è contento.
Questo è il modo più comune per correggere un errore di prestito: cambiare l’ordine. Fai tutte le letture che ti servono, poi la scrittura (o viceversa), invece di mescolarle.
Perché è importante
Sezione intitolata “Perché è importante”Queste regole vengono controllate interamente in fase di compilazione, quindi non costano niente quando il programma gira. È così che Rust può promettere «niente data race» ed essere comunque veloce come il C: la sicurezza è dimostrata prima ancora che il programma parta.
Pratica
Sezione intitolata “Pratica”Prova prima da solo ogni esercizio, nel Rust Playground o in un progetto locale. Se il compilatore protesta, leggi l’errore: di solito mostra esattamente dove i prestiti si sovrappongono.
1. Conta senza prendere
Sezione intitolata “1. Conta senza prendere”Scrivi una funzione total_letters che prende in prestito un Vec<String> e restituisce quante lettere hanno in tutto le parole. Dopo la chiamata, main deve poter ancora stampare il vettore.
Suggerimento: il tipo del parametro è &Vec<String> e lo chiami con &words. Scorri il vettore preso in prestito con for word in list e somma word.len().
Mostra la soluzione
fn total_letters(list: &Vec<String>) -> usize { let mut total = 0; for word in list { total += word.len(); } total}
fn main() { let words = vec![String::from("prestito"), String::from("controllo")]; let count = total_letters(&words); // presta il vettore println!("{count} lettere"); println!("{:?} appartiene ancora a main", words);}Output:
17 lettere["prestito", "controllo"] appartiene ancora a mainLa funzione deve solo guardare le parole, quindi basta un prestito di sola lettura con &. Se invece scrivessi list: Vec<String>, il vettore si sposterebbe nella funzione e l’ultimo println! non compilerebbe. (Le parole non hanno accenti apposta: len() conta i byte, e una lettera accentata ne occupa due.)
2. La lista della spesa
Sezione intitolata “2. La lista della spesa”Scrivi due funzioni: add_item, che aggiunge un articolo alla lista della spesa, e print_list, che stampa ogni articolo. Aggiungi tre articoli, poi stampa la lista.
Suggerimento: una funzione modifica la lista e l’altra la legge soltanto. Quale ha bisogno di &mut Vec<String> e a quale basta &Vec<String>? Per trasformare un &str in una String, usa String::from(item).
Mostra la soluzione
fn add_item(list: &mut Vec<String>, item: &str) { list.push(String::from(item));}
fn print_list(list: &Vec<String>) { for item in list { println!("- {item}"); }}
fn main() { let mut list = Vec::new(); add_item(&mut list, "latte"); add_item(&mut list, "pane"); add_item(&mut list, "mele"); print_list(&list);}Output:
- latte- pane- meleadd_item modifica la lista, quindi riceve un prestito &mut, e list in main deve essere mut. print_list legge soltanto, quindi basta un semplice &. Ogni prestito &mut list finisce quando la sua chiamata ritorna: per questo la chiamata successiva può prenderla di nuovo in prestito.
3. Due scrittori insieme
Sezione intitolata “3. Due scrittori insieme”Questo programma non compila. Correggilo in modo che stampi Punteggio: 10 punti, tenendo sia first sia second.
fn main() { let mut message = String::from("Punteggio:"); let first = &mut message; let second = &mut message; first.push_str(" 10"); second.push_str(" punti"); println!("{message}");}error[E0499]: cannot borrow `message` as mutable more than once at a time --> src/main.rs:4:18 |3 | let first = &mut message; | ------------ first mutable borrow occurs here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | first.push_str(" 10"); | ----- first borrow later used hereSuggerimento: un solo scrittore alla volta. Rileggi Quando finisce un prestito? qui sopra: riesci a finire di usare first prima di creare second?
Mostra la soluzione
fn main() { let mut message = String::from("Punteggio:");
let first = &mut message; first.push_str(" 10"); // ultimo uso di first: il suo prestito finisce qui
let second = &mut message; second.push_str(" punti");
println!("{message}");}Output:
Punteggio: 10 puntiNell’originale, first doveva ancora essere usato (alla riga 5) quando second veniva creato alla riga 4. Sono due scrittori nello stesso momento. Spostando in alto first.push_str, first ha finito prima che second esista, quindi c’è sempre un solo &mut alla volta.
Riepilogo
Sezione intitolata “Riepilogo”- Il prestito (
&) permette al codice di usare un valore senza prenderne l’ownership: poi ti torna indietro. - Un semplice riferimento
&è di sola lettura; usa&mutper modificare il valore preso in prestito. - Per modificare tramite un riferimento, la variabile, il parametro e la chiamata hanno tutti bisogno di
mut/&mut. - La regola: tanti lettori oppure un solo scrittore, mai entrambi insieme.
- Un prestito finisce all’ultimo uso del riferimento, quindi spesso basta riordinare il codice per correggere un errore di prestito.
- Tutto questo è controllato in fase di compilazione, quindi a runtime non costa nulla.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa fa & davanti a un valore quando lo passi a una funzione?
2. Secondo le regole del prestito, quale combinazione è permessa nello stesso momento?
3. Cosa ti serve per modificare un valore tramite un riferimento?
4. Perché Rust può far rispettare le regole del prestito senza alcun costo a runtime?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- References and Borrowing — The Rust Book (in inglese): il capitolo ufficiale, che parla anche dei riferimenti pendenti (dangling) e del perché Rust si rifiuta di crearli.
- Borrowing — Rust by Example (in inglese): esempi eseguibili di prestiti con
&, con pagine successive sui prestiti mutabili e sull’aliasing. - Non-lexical lifetimes — The Edition Guide (in inglese): la novità che ha fatto finire i prestiti al loro ultimo uso invece che alla parentesi graffa di chiusura.
- The Slice Type — The Rust Book (in inglese): prendere in prestito solo una parte di una stringa o di un array, il passo naturale dopo i riferimenti.
reference— documentazione della libreria standard (in inglese): cosa sono&Te&mut Te quali trait implementano.