Reference e Borrowing
Il problema con il codice nel Listato 4-5 è che dobbiamo restituire la String
alla funzione chiamante in modo da poter ancora utilizzare la String
dopo la
chiamata a calcola_lunghezza
, perché la String
è stata spostata in
calcola_lunghezza
. Possiamo invece fornire un riferimento (reference) al
valore String
. Un reference è come un puntatore in quanto è un indirizzo che
possiamo seguire per accedere ai dati archiviati a quell’indirizzo di memoria;
la ownership di quei dati appartiene ad un’altra variabile. A differenza di un
puntatore, è garantito che un reference punti a un valore valido di un certo
type finché il reference è ancora valido.
Ecco come definiresti e utilizzeresti una funzione calcola_lunghezza
che abbia
un reference ad un oggetto come parametro invece di assumere la ownership
del valore:
fn main() { let s1 = String::from("ciao"); let lung = calcola_lunghezza(&s1); println!("La lunghezza di '{s1}' è {lung}."); } fn calcola_lunghezza(s: &String) -> usize { s.len() }
Innanzitutto, nota che tutto il codice che dichiarava e ritornava la variabile
tupla è sparito. In secondo luogo, nota che passiamo &s1
a calcola_lunghezza
e, nella sua definizione del parametro, prendiamo &String
anziché String
. Il
carattere &
(E commerciale) rappresenta il reference e consente di fare
riferimento a un valore senza prenderne la ownership.
La Figura 4-6 illustra questo concetto.
Figura 4-6: Schema di &String
s
che punta a String
s1
Nota: l’opposto della referenziazione tramite l’uso di
&
è la de-referenziazione, che si realizza con l’operatore di de-referenziazione*
(dereference operator). Vedremo alcuni usi dell’operatore di de-referenziazione nel Capitolo 8 e discuteremo i dettagli della de-referenziazione nel Capitolo 15.
Diamo un’occhiata più da vicino alla chiamata di funzione:
fn main() { let s1 = String::from("ciao"); let lung = calcola_lunghezza(&s1); println!("La lunghezza di '{s1}' è {lung}."); } fn calcola_lunghezza(s: &String) -> usize { s.len() }
La sintassi &s1
ci permette di creare un reference che punta al valore di
s1
ma non lo possiede. Poiché il reference non lo possiede, il valore a
cui punta non verrà rilasciato dalla memoria quando il reference smette di
essere utilizzato. Allo stesso modo, la firma della funzione utilizza &
per
indicare che il type del parametro s
è un reference. Aggiungiamo alcune
annotazioni esplicative:
fn main() { let s1 = String::from("ciao"); let lung = calcola_lunghezza(&s1); println!("La lunghezza di '{s1}' è {lung}."); } fn calcola_lunghezza(s: &String) -> usize { // `s` è un reference a una String s.len() } // Qui, `s` esce dallo scope. Ma siccome `s` non ha ownership di quello // a cui fa riferimento, i valori di String non vengono cancellati
Lo scope in cui la variabile s
è valida è lo stesso dello scope di
qualsiasi parametro di funzione, ma il valore a cui punta il reference non
viene eliminato quando s
smette di essere utilizzato, perché s
non ha la
ownership. Quando le funzioni hanno reference come parametri anziché valori
effettivi, non avremo bisogno di restituire i valori per restituire la
ownership, perché la ownership non ci è mai stata trasferita.
L’azione di creare un reference viene chiamata borrowing (fare un prestito in italiano). Come nella vita reale, se una persona possiede qualcosa, puoi chiedergliela in prestito. Quando hai finito, devi restituirla. Non la possiedi.
Quindi, cosa succede se proviamo a modificare qualcosa che abbiamo in prestito? Prova il codice nel Listato 4-6. Avviso spoiler: non funziona!
fn main() {
let s = String::from("hello");
cambia(&s);
}
fn cambia(una_stringa: &String) {
una_stringa.push_str(", world");
}
Ecco l’errore:
$ cargo run
Compiling ownership v0.1.0 (file:///progetti/ownership)
error[E0596]: cannot borrow `*una_stringa` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | una_stringa.push_str(", world");
| ^^^^^^^^^^^ `una_stringa` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn cambia(una_stringa: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Così come le variabili sono immutabili come impostazione predefinita, lo sono anche i reference. Non possiamo modificare qualcosa a cui abbiamo solo un riferimento.
Reference Mutabili
Possiamo correggere il codice del Listato 4-6 per permetterci di modificare un valore preso in prestito con alcune piccole modifiche che utilizzano, invece, un reference mutabile:
fn main() { let mut s = String::from("hello"); cambia(&mut s); } fn cambia(una_stringa: &mut String) { una_stringa.push_str(", world"); }
Per prima cosa rendiamo s
mutabile con mut
. Poi creiamo un reference
mutabile con &mut s
dove chiamiamo la funzione cambia
e aggiorniamo la firma
della funzione in modo che accetti un reference mutabile con una_stringa: &mut String
. In questo modo è molto chiaro che la funzione cambia
muterà il
valore che prende in prestito. I reference mutabili hanno una grande
restrizione: se hai un reference mutabile a un valore, non puoi avere altri
reference a quel valore. Questo codice che tenta di creare due reference
mutabili a s
fallirà:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
Ecco l’errore:
$ cargo run
Compiling ownership v0.1.0 (file:///progetti/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:6:14
|
5 | let r1 = &mut s;
| ------ first mutable borrow occurs here
6 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}");
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
L’errore dice che questo codice non è valido perché non possiamo avere più di un
reference mutabile alla volta ad s
. Il primo reference mutabile è in r1
e deve durare fino a quando non viene utilizzato nel println!
, ma tra la
creazione di quel reference mutabile e il suo utilizzo, abbiamo cercato di
creare un altro reference mutabile in r2
che prende in prestito gli stessi
dati di r1
.
La restrizione che impedisce più reference mutabili agli stessi dati contemporaneamente consente la mutazione ma in modo molto controllato. È qualcosa con cui chi comincia a programmare in Rust fatica perché la maggior parte dei linguaggi ti consente di mutare quando vuoi. Il vantaggio di avere questa restrizione è che Rust può prevenire conflitti di accesso ai dati, data race, in fase di compilazione. Una data race è simile a una condizione di competizione e si verifica quando si verificano questi tre comportamenti:
- Due o più puntatori accedono contemporaneamente agli stessi dati.
- Almeno uno dei puntatori viene utilizzato per scrivere nei dati.
- Non viene utilizzato alcun meccanismo per sincronizzare l’accesso ai dati
I data race causano comportamenti non programmati e possono essere difficili da diagnosticare e risolvere quando si cerca di individuarli durante l’esecuzione; Rust previene questo problema rifiutando di compilare codice contenente data race!
Come sempre, possiamo usare le parentesi graffe per creare uno scope nuovo, consentendo di avere più reference mutabili, ma non simultanee:
fn main() { let mut s = String::from("ciao"); { let r1 = &mut s; } // qui `r1` esce dallo scope, quindi possiamo creare // un nuovo reference senza problemi let r2 = &mut s; }
Rust applica una regola simile per combinare reference mutabili e immutabili. Questo codice genera un errore:
fn main() {
let mut s = String::from("ciao");
let r1 = &s; // nessun problema
let r2 = &s; // nessun problema
let r3 = &mut s; // GROSSO PROBLEMA
println!("{r1}, {r2}, e {r3}");
}
Ecco l’errore:
$ cargo run
Compiling ownership v0.1.0 (file:///progetti/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:7:14
|
5 | let r1 = &s; // nessun problema
| -- immutable borrow occurs here
6 | let r2 = &s; // nessun problema
7 | let r3 = &mut s; // GROSSO PROBLEMA
| ^^^^^^ mutable borrow occurs here
8 |
9 | println!("{r1}, {r2}, e {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Wow! Non possiamo nemmeno avere un reference mutabile mentre ne abbiamo uno immutabile allo stesso valore.
Chi userà un reference immutabile non si aspetta certo che il valore cambi improvvisamente! Tuttavia, sono consentiti reference multipli immutabili perché nessuno che stia leggendo i dati ha la possibilità di influenzare la lettura dei dati da parte di altri.
Nota che lo scope di un reference inizia dal punto in cui viene introdotto e
continua fino all’ultima volta che viene utilizzato. Ad esempio, questo codice
verrà compilato perché l’ultimo utilizzo dei reference immutabili avviene nel
println!
, prima che venga introdotto il reference mutabile:
fn main() { let mut s = String::from("ciao"); let r1 = &s; // nessun problema let r2 = &s; // nessun problema println!("{r1} and {r2}"); // Le variabili `r1` e `r2` non verranno più usato dopo questo punto let r3 = &mut s; // nessun problema println!("{r3}"); }
Gli scope dei reference immutabili r1
e r2
terminano dopo il println!
in cui sono stati utilizzati per l’ultima volta, ovvero prima che venga creato
il reference mutabile r3
. Questi scope non si sovrappongono, quindi questo
codice è consentito: il compilatore capisce che il reference non verrà più
utilizzato in nessun altro punto prima della fine dello scope.
Anche se a volte gli errori di borrowing possono essere frustranti, ricorda che è il compilatore di Rust a segnalare un potenziale bug in anticipo (in fase di compilazione e non in fase di esecuzione) e a mostrarti esattamente dove si trova il problema. In questo modo non dovrai cercare di capire perché i tuoi dati non sono quelli che pensavi fossero quando il programma è in esecuzione.
Reference Pendenti
Nei linguaggi con puntatori, è facile creare erroneamente un puntatore pendente, cioè un puntatore che fa riferimento a una posizione in memoria non più valido, perché quella memoria assegnata a quella variabile è stata liberata, ma non si è provveduto a cancellare anche il puntatore che per l’appunto rimane pendente puntando a qualcosa che non è più disponibile. In Rust, al contrario, il compilatore garantisce che i reference non diverranno mai pendenti: se si ha un reference ad alcuni dati, il compilatore ci impedirà di usare quel reference dopo che i dati sono usciti dallo scope.
Proviamo a creare un reference pendente per vedere come Rust li previene segnalando un errore in fase di compilazione:
fn main() {
let reference_a_nulla = pendente();
}
fn pendente() -> &String {
let s = String::from("ciao");
&s
}
Ecco l’errore:
$ cargo run
Compiling ownership v0.1.0 (file:///progetti/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:18
|
5 | fn pendente() -> &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
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 pendente() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn pendente() -> &String {
5 + fn pendente() -> String {
|
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:8:5
|
8 | &s
| ^^ returns a reference to data owned by the current function
Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous error
Questo messaggio di errore si riferisce a una funzionalità che non abbiamo ancora trattato: la longevità (lifetime d’ora in poi). Parleremo in dettaglio della lifetime nel Capitolo 10. Ma, se trascuriamo le parti relative alla lifetime, il messaggio contiene la chiave del motivo per cui questo codice è un problema:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
(traduzione: il type di ritorno di questa funzione contiene un valore in
prestito, ma non c’è alcun valore da cui prenderlo in prestito)
Diamo un’occhiata più da vicino a cosa succede esattamente in ogni fase della
nostra funzione pendente
:
fn main() {
let reference_a_nulla = pendente();
}
fn pendente() -> &String { // pendente ritorna un reference a String
let s = String::from("ciao"); // `s` è una String nuova
&s // ritorniamo un reference alla String `s`
} // Qui `s` esce dallo scope e viene cancellata, così come la memora assegnatale.
// Pericolo!
Poiché s
viene creato all’interno di pendente
, quando il codice di
pendente
sarà terminato, s
e la memoria ad essa assegnata verranno
rilasciate. Ma abbiamo cercato di restituire un reference a questa memoria.
Ciò significa che questo reference punterebbe a una String
non valida.
Questo non va bene! Rust non ci permette di farlo.
La soluzione è restituire direttamente la String
:
fn main() { let stringa = non_pendente(); } fn non_pendente() -> String { let s = String::from("ciao"); s }
Questo funziona senza problemi: la ownership viene spostata all’esterno e non viene rilasciato nulla.
Le Regole dei Reference
Ricapitoliamo quello che abbiamo detto sui reference:
- In un dato momento, puoi avere o un singolo reference mutabile o un numero qualsiasi di reference immutabili.
- I reference devono essere sempre validi.
Successivamente, analizzeremo un’altra tipologia di reference: le sezioni (slice in inglese).