Il borrow checker
Ownership e funzioni
Il passaggio degli argomenti alle funzioni, e il valore di ritorno seguono le stesse regole della ownership, similarmente agli assegnamenti. Questo vuol dire che se per esempio passiamo una stringa a una funzione, quella ne prende proprietà.
fn main() { let name = String::from("Mario"); // la proprietà della stringa è trasferita quì greet(name); // quì non è più possibile accedere a name } fn greet(x: String) { // x prende la ownership println!("Ciao {x}"); } // x va out of scope quì, quindi la stringa viene eliminata
Un modo per trasferire la proprietà indietro da una funzione è quello di ritornare l'oggetto al chiamante.
fn main() { let name = String::from("Mario"); // la proprietà della stringa è trasferita quì, ma viene ripresa e trasferita in name2 let name2 = greet(name); // quì non è più possibile accedere a name ma è possibile accedere a name2 println!("name2: {name2}"); } fn greet(x: String) -> String { println!("Ciao {x}"); return x; }
Chiaramente questo è oneroso e non pratico, ma Rust offre un modo per "prestare" un oggetto a una variabile in modo che ci possa accedere, ma che non prenda ownership dell'oggetto. Questa feature si chiama borrowing.
Borrowing
Possiamo creare una reference a un oggetto, che intuitivamente è come un puntatore a quell'oggetto, ma senza prendere ownership del valore. La stessa funzione di sopra può essere scritta così:
fn main() { let name = String::from("Mario"); // passiamo una reference con & greet(&name); // name ha ancora ownership della stringa println!("name: {name}"); } fn greet(x: &String) { println!("Ciao {x}"); } // x esce dallo scope, ma x è solo una reference e quindi // la stinga non viene eliminata
Possiamo creare una reference tramite l'operatore &, il suo inverso è l'operatore di de-referenziazione *, ma tipicamente Rust riesce a inferire il livello d'indirezione, così possiamo lavorare trasparentemente con le reference.
Alla chiamata della funzione, viene passata una reference e il valore viene "prestato" alla variabile x.
Quando questa esce dallo scope alla fine della funzione greet, il valore viene restituito indietro.
Mutable reference
Cosa succede se proviamo a modificare un valore tramite una reference?
fn main() { let mut name = String::from("Hello"); make_greet(&name); println!("name: {name}"); } fn make_greet(x: &String) { x.push_str(" World!"); }
Nonostante la variabile originaria sia mutabile, otteniamo un errore:
error[E0596]: cannot borrow `*x` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | x.push_str(" World!");
| ^ `x` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
Questo perché la reference stessa deve essere marcata come mutabile, o in altre parole con solo l'operatore & stiamo creando una reference immutabile.
Come al solito il compilatore ci suggerisce un fix:
help: consider changing this to be a mutable reference
|
7 | fn make_greet(x: &mut String) {
| +++
ovvero quello di cambiare il tipo di reference a una reference mutabile. Le reference mutabili possono accedere al valore sia in lettura che in scrittura, e quindi in particolare possono modificare il valore originale.
fn main() { let mut name = String::from("Hello"); make_greet(&mut name); println!("name: {name}"); } fn make_greet(x: &mut String) { x.push_str(" World!"); }
Notiamo che abbiamo dovuto dichiarare la funzione che accetti una reference mutabile &mut, e poi abbiamo dovuto creare esplicitamente una reference mutabile con l'operatore &mut.
In questo modo è chiaro sia al chiamante che alla funzione che quel particolare valore è acceduto anche in scrittura.
Equivalentemente, se passiamo un valore come reference immutabile a una funzione, Rust garantisce che quel valore non verrà modificato dalla funzione.
Il borrow checker
Le reference seguono due regole che ci aiutano a definire un linguaggio safe, ovvero:
- per ogni oggetto è possibile avere a ogni momento più reference immutabili oppure una sola reference mutabile,
- ogni reference deve essere valida per tutta la sua vita.
La prima regola è una diretta applicazione del principio "single writer, multiple readers", ed è uno dei pilastri con cui Rust riesce per esempio a gestire la concorrenza in maniera sicura. In particolare questa regola implica che by-design non è possibile creare una race condition, perché richiederebbe che due thread abbiano accesso mutabile allo stesso oggetto. Ovviamente a volte questa situazione è inevitabile, ad esempio se vogliamo implementare una coda condivisa tra thread, ma Rust offre dei meccanismi per gestire anche queste situazioni che vedremo in un prossimo capitolo.
Vediamo alcuni esempi di questo principio:
fn main() { let mut x = String::from("Test"); // possiamo creare più reference immutabili senza problemi let r1 = &x; let r2 = &x; println!("r1: {r1}, r2: {r2}"); // r1 e r2 non sono più usati da questo punto in poi // quindi è possibile creare una reference mutabile // (mr non coesiste con r1 e r2) let mr = &mut x; mr.push_str(" String"); let mr2 = &mut x; // ^ errore di compilazione // perché mr è acceduta anche alla riga sotto // quindi a questo punto coesisterebbero due reference // mutabili mr e mr2 println!("mr: {mr}, mr2:{mr2}"); }
L'errore ci riporta che non possiamo "prestare" la variabile x in maniera mutabile più di una volta contemporaneamente. e ci presenta dove vengono definite le reference.
error[E0499]: cannot borrow `x` as mutable more than once at a time
--> src/main.rs:15:15
|
12 | let mr = &mut x;
| ------ first mutable borrow occurs here
...
15 | let mr2 = &mut x; // <- errore di compilazione
| ^^^^^^ second mutable borrow occurs here
...
19 | println!("mr {mr}");
| ---- first borrow later used here
Dangling references
In un linguaggio come Java o c++ sarebbe molto facile creare reference o puntatori a oggetti invalidi o che non esistono più, perché i linguaggi non lo impediscono. In Rust questo non è possibile:
fn main() { let r = return_ref(); } fn return_ref() -> &String { let x = String::from("Test"); &x // ritorna una reference a x, ma non trasferisce l'ownership // quindi l'oggetto sarebbe eliminato alla fine della funzione }
Il compilatore ci restituisce un errore, dicendo che stiamo ritornando una reference a un valore che non esisterà più.
error[E0106]: missing lifetime specifier
--> src/main.rs:5:20
|
5 | fn return_ref() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
Come al solito il compilatore ci fornisce dei possibili fix, e suggerisce che probabilmente vogliamo ritornare anche la ownership della stringa al chiamante.
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn return_ref() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn return_ref() -> &String {
5 + fn return_ref() -> String {
|
Una altra possibile soluzione è quella di indicare esplicitamente la lifetime della reference, cosa che permette di specificare al compilatore il "periodo di validità" della reference. Vedremo le lifetime in un prossimo capitolo.