Riferimenti e Borrowing
Il problema con il codice della tupla nel Listato 4-5 è che dobbiamo restituire il String
alla funzione chiamante in modo da poter ancora usare il String
dopo la chiamata a calculate_length
, perché il String
è stato mosso in calculate_length
. Invece, possiamo fornire un riferimento al valore String
. Un riferimento è come un puntatore nel senso che è un indirizzo che possiamo seguire per accedere ai dati memorizzati in quell'indirizzo; quei dati sono di proprietà di qualche altra variabile. A differenza di un puntatore, un riferimento è garantito puntare a un valore valido di un tipo particolare per tutta la durata di quel riferimento.
Ecco come definiresti e useresti una funzione calculate_length
che ha un riferimento a un oggetto come parametro invece di prendere Ownership del valore:
Filename: src/main.rs
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
Per prima cosa, nota che tutto il codice della tupla nella dichiarazione della variabile e il valore restituito della funzione è sparito. Secondo, nota che passiamo &s1
in calculate_length
e, nella sua definizione, prendiamo &String
anziché String
. Questi simboli di ampersand rappresentano i riferimenti, e ti permettono di fare riferimento a qualche valore senza prenderne l'Ownership. La Figura 4-5 illustra questo concetto.
Nota: L'opposto del riferimento utilizzando
&
è il dereferenziare, che si realizza con l'operatore di dereferenziazione,*
. Vedremo alcuni utilizzi dell'operatore di dereferenziazione nel Capitolo 8 e discuteremo i dettagli della dereferenziazione nel Capitolo 15.
Diamo uno sguardo più da vicino alla chiamata della funzione qui:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
La sintassi &s1
ci permette di creare un riferimento che si riferisce al valore di s1
ma non ne prende l'Ownership. Poiché non ha l'Ownership, il valore a cui punta non verrà eliminato quando il riferimento smette di essere usato.
Allo stesso modo, la firma della funzione usa &
per indicare che il tipo del parametro s
è un riferimento. Aggiungiamo alcune annotazioni esplicative:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because it does not have ownership of what // it refers to, it is not dropped.
Lo Scope in cui la variabile s
è valida è lo stesso di qualsiasi parametro di funzione, ma il valore a cui si punta attraverso il riferimento non viene eliminato quando s
smette di essere usato, perché s
non ha l'Ownership. Quando le funzioni hanno riferimenti come parametri anziché i valori effettivi, non avremo bisogno di restituire i valori per restituire l'Ownership, perché non abbiamo mai avuto l'Ownership.
Chiamiamo l'azione di creare un riferimento borrowing. Come nella vita reale, se una persona possiede qualcosa, puoi prenderla in prestito. Quando hai finito, devi restituirla. Non ne possiedi l'Ownership.
Quindi, cosa succede se proviamo a modificare qualcosa che stiamo prendendo in prestito? Prova il codice nel Listato 4-6. Avviso spoiler: non funziona!
Filename: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Ecco l'errore:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error
Proprio come le variabili sono immutabili di default, lo sono anche i riferimenti. Non ci è consentito modificare qualcosa a cui stiamo facendo riferimento.
Riferimenti Mutabili
Possiamo correggere il codice dal Listato 4-6 per permetterci di modificare un valore preso in prestito con solo alcune piccole modifiche che utilizzano, invece, un riferimento mutabile:
Filename: src/main.rs
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
Per prima cosa, cambiamo s
per essere mut
. Poi creiamo un riferimento mutabile con &mut s
dove chiamiamo la funzione change
, e aggiorniamo la firma della funzione per accettare un riferimento mutabile con some_string: &mut String
. Questo rende molto chiaro che la funzione change
modificherà il valore che prende in prestito.
I riferimenti mutabili hanno una grande restrizione: se hai un riferimento mutabile a un valore, non puoi avere altri riferimenti a quel valore. Questo codice che tenta di creare due riferimenti mutabili a s
fallirà:
Filename: src/main.rs
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:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error
Questo errore dice che questo codice è invalido perché non possiamo prendere in prestito s
come mutabile più di una volta alla volta. Il primo borrow mutabile è in r1
e deve durare fino a quando viene utilizzato nel println!
, ma tra la creazione di quel riferimento mutabile e il suo utilizzo, abbiamo provato a creare un altro riferimento mutabile in r2
che prende in prestito gli stessi dati di r1
.
La restrizione che impedisce riferimenti mutabili multipli agli stessi dati nello stesso momento permette la mutazione ma in modo molto controllato. È qualcosa con cui i nuovi Rustaceans lottano perché la maggior parte dei linguaggi ti permette di mutare quando vuoi. Il vantaggio di avere questa restrizione è che Rust può prevenire i data race al tempo di compilazione. Un data race è simile a una race condition e si verifica quando questi tre comportamenti si verificano:
- Due o più puntatori accedono agli stessi dati contemporaneamente.
- Almeno uno dei puntatori viene utilizzato per scrivere i dati.
- Non c'è un meccanismo utilizzato per sincronizzare l'accesso ai dati.
I data race causano comportamento indefinito e possono essere difficili da diagnosticare e correggere quando si cerca di rintracciarli a runtime; Rust impedisce questo problema rifiutando di compilare codice con data race!
Come sempre, possiamo usare le parentesi graffe per creare un nuovo Scope, permettendo riferimenti mutabili multipli, ma non simultanei:
fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s; }
Rust applica una regola simile per combinare riferimenti mutabili e immutabili. Questo codice risulta in un errore:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
}
Ecco l'errore:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
Uff! Non possiamo neanche avere un riferimento mutabile mentre abbiamo un riferimento immutabile allo stesso valore.
Gli utenti di un riferimento immutabile non si aspettano che il valore cambi improvvisamente sotto di loro! Tuttavia, riferimenti immutabili multipli sono consentiti perché nessuno che sta solo leggendo i dati ha la capacità di influenzare la lettura dei dati da parte di qualcun altro.
Nota che lo Scope di un riferimento inizia dal punto in cui viene introdotto e continua attraverso l'ultima volta che quel riferimento viene usato. Per esempio, questo codice compilerà perché l'ultimo uso dei riferimenti immutabili, il println!
, avviene prima che venga introdotto il riferimento mutabile:
fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{} and {}", r1, r2); // variables r1 and r2 will not be used after this point let r3 = &mut s; // no problem println!("{}", r3); }
Lo Scope dei riferimenti immutabili r1
e r2
termina dopo il println!
dove vengono usati per l'ultima volta, che è prima che il riferimento mutabile r3
venga creato. Questi Scope non si sovrappongono, quindi questo codice è consentito: il compilatore può dire che il riferimento non è più usato in un punto prima della fine dello Scope.
Anche se gli errori di borrowing possono essere frustranti a volte, ricorda che è il compilatore Rust a evidenziare un potenziale bug in anticipo (al tempo di compilazione piuttosto che a runtime) e mostrandoti esattamente dove si trova il problema. In questo modo non devi rintracciare il motivo per cui i dati non sono ciò che pensavi.
Riferimenti Dangling
Nei linguaggi con puntatori, è facile creare erroneamente un puntatore dangling—un puntatore che si riferisce a una posizione in memoria che potrebbe essere stata assegnata a qualcun altro—liberando qualche memoria mentre si mantiene un puntatore a quella memoria. In Rust, al contrario, il compilatore garantisce che i riferimenti non saranno mai riferimenti dangling: se hai un riferimento a qualche dato, il compilatore si assicurerà che i dati non escano dallo Scope prima del riferimento ai dati.
Proviamo a creare un riferimento dangling per vedere come Rust li previene con un errore al tempo di compilazione:
Filename: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Ecco l'errore:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &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
|
5 | fn dangle() -> &'static String {
| +++++++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
Questo messaggio di errore si riferisce a una caratteristica che non abbiamo ancora trattato: i lifetimes. Discuteremo i lifetimes in dettaglio nel Capitolo 10. Ma, se ignori le parti sui lifetimes, il messaggio contiene la chiave del perché questo codice è un problema:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
Diamo uno sguardo più approfondito a cosa sta succedendo esattamente in ogni fase del nostro codice dangle
:
Filename: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!
Poiché s
è creato all'interno di dangle
, quando il codice di dangle
è finito, s
sarà deallocato. Ma abbiamo provato a restituire un riferimento a esso. Ciò significa che questo riferimento punterebbe a un String
non valido. Questo non va bene! Rust non ci permetterà di farlo.
La soluzione qui è restituire il String
direttamente:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
Questo funziona senza problemi. L'Ownership viene trasferita, e nulla viene deallocato.
Le Regole dei Riferimenti
Facciamo un riepilogo di ciò che abbiamo discusso riguardo ai riferimenti:
- In qualsiasi momento, puoi avere o un solo riferimento mutabile o un qualsiasi numero di riferimenti immutabili.
- I riferimenti devono essere sempre validi.
Successivamente, esamineremo un diverso tipo di riferimento: i slices.