Riferimenti e Borrowing

Il problema con il codice della tupla in Listing 4-5 è che dobbiamo restituire lo String alla funzione chiamante affinché possiamo ancora usare lo String dopo la chiamata a calculate_length, poiché lo String è stato spostato in calculate_length. Invece, possiamo fornire un riferimento al valore dello String. Un riferimento è come un puntatore in quanto è 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 per puntare a un valore valido di un particolare tipo per la durata di quel riferimento.

Ecco come definiremo e utilizzeremo una funzione calculate_length che ha un riferimento a un oggetto come parametro anziché prendere possesso del valore:

Nome del file: 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()
}

Innanzitutto, nota che tutto il codice della tupla nella dichiarazione della variabile e il valore di ritorno della funzione è sparito. In secondo luogo, nota che passiamo &s1 in calculate_length e, nella sua definizione, prendiamo &String anziché String. Questi simboli di e commerciale rappresentano i riferimenti e permettono di riferirsi a un valore senza prenderne possesso. La Figura 4-5 illustra questo concetto.

Tre tabelle: la tabella per s contiene solo un puntatore alla tabella per s1. La tabella per s1 contiene i dati nello stack per s1 e punta ai dati della stringa nello heap.

Figura 4-5: Un diagramma di &String s che punta a String s1

Nota: L'opposto di riferirsi usando & è dereferenziare, che si ottiene con l'operatore di dereferenziazione, *. Vedremo alcuni usi dell'operatore di dereferenziazione nel Capitolo 8 e discuteremo i dettagli della dereferenziazione nel Capitolo 15.

Osserviamo più da vicino la 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 consente di creare un riferimento che si riferisce al valore di s1 ma non ne ha il possesso. Poiché non ne ha il possesso, il valore a cui punta non verrà eliminato quando il riferimento smette di essere utilizzato.

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 come per qualsiasi parametro di funzione, ma il valore puntato dal riferimento non viene eliminato quando s smette di essere utilizzato, perché s non ne ha il possesso. Quando le funzioni hanno riferimenti come parametri anziché i valori effettivi, non avremo bisogno di restituire i valori per restituire il possesso, perché non ne avevamo mai il possesso.

Chiamiamo l'azione di creare un riferimento borrowing. Come nella vita reale, se una persona possiede qualcosa, puoi prenderlo in prestito da loro. Quando hai finito, devi restituirlo. Non lo possiedi.

Quindi, cosa succede se proviamo a modificare qualcosa che abbiamo in prestito? Prova il codice in Listing 4-6. Spoiler: non funziona!

Nome del file: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Listing 4-6: Tentativo di modificare un valore preso in prestito

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, anche i riferimenti lo sono. Non ci è permesso modificare qualcosa a cui abbiamo un riferimento.

Riferimenti Mutabili

Possiamo correggere il codice da Listing 4-6 per permetterci di modificare un valore preso in prestito con solo pochi piccoli aggiustamenti che utilizzano, invece, un riferimento mutabile:

Nome del file: 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");
}

Prima cambiamo s in 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à:

Nome del file: 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 non è valido perché non possiamo prendere in prestito s come mutabile più di una volta alla volta. Il primo prestito mutabile è in r1 e deve durare fino a quando viene utilizzato nel println!, ma tra la creazione di quel riferimento mutabile e il suo uso, abbiamo tentato di creare un altro riferimento mutabile in r2 che prende in prestito gli stessi dati di r1.

La restrizione che impedisce molteplici riferimenti mutabili agli stessi dati contemporaneamente permette la mutazione ma in modo molto controllato. È qualcosa con cui i nuovi Rustaceans lottano perché la maggior parte delle lingue ti permette di mutare quando vuoi. Il vantaggio di avere questa restrizione è che Rust può prevenire le data races in fase di compilazione. Una 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'è nessun meccanismo che viene usato per sincronizzare l'accesso ai dati.

Le data races causano comportamenti indefiniti e possono essere difficili da diagnosticare e correggere quando si cerca di scovare i problemi a runtime; Rust previene questo problema rifiutandosi di compilare codice con data races!

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 produce 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

Uffa! Anche non possiamo avere un riferimento mutabile mentre abbiamo uno immutabile allo stesso valore.

Gli utenti di un riferimento immutabile non si aspettano che il valore cambi improvvisamente! Tuttavia, sono permessi riferimenti immutabili multipli perché nessuno che sta solo leggendo i dati ha la capacità di influenzare la lettura dei dati di qualcun altro.

Nota che lo scope di un riferimento inizia da dove viene introdotto e continua fino all'ultima volta che quel riferimento viene utilizzato. Per esempio, questo codice compilerà perché l'ultimo utilizzo dei riferimenti immutabili, il println!, si verifica prima che il riferimento mutabile venga introdotto:

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);
}

Gli scope dei riferimenti immutabili r1 e r2 terminano dopo il println! dove vengono utilizzati per l'ultima volta, che è prima che il riferimento mutabile r3 venga creato. Questi scope non si sovrappongono, quindi questo codice è permesso: il compilatore può dire che il riferimento non viene più utilizzato in un punto prima della fine dello scope.

Anche se gli errori di borrowing possono essere frustranti a volte, ricorda che è il compilatore di Rust a indicare un potenziale bug in anticipo (in fase di compilazione piuttosto che a runtime) e a mostrarti esattamente dove si trova il problema. Poi non devi rintracciare perché i tuoi dati non sono quelli che pensavi fossero.

Riferimenti Pendenti

Nelle lingue con puntatori, è facile creare erroneamente un puntatore pendente—un puntatore che fa riferimento a una posizione in memoria che potrebbe essere stata assegnata a qualcun altro—liberando un po' di memoria mentre si conserva ancora un puntatore a quella memoria. In Rust, al contrario, il compilatore garantisce che i riferimenti non saranno mai riferimenti pendenti: se hai un riferimento a qualche dato, il compilatore assicurerà che i dati non usciranno dallo scope prima che il riferimento ai dati lo faccia.

Proviamo a creare un riferimento pendente per vedere come Rust li impedisce con un errore in fase di compilazione:

Nome del file: 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 funzione che non abbiamo ancora trattato: 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

Osserviamo più da vicino cosa sta succedendo ad ogni fase del nostro codice dangle:

Nome del file: 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 è terminato, s verrà deallocato. Ma abbiamo tentato di restituire un riferimento ad esso. Ciò significa che questo riferimento punterebbe a una String non valida. Non va bene! Rust non ce lo permette.

La soluzione qui è restituire direttamente la String:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Questo funziona senza problemi. Il possesso viene spostato fuori, e niente viene deallocato.

Le Regole dei Riferimenti

Riassumiamo ciò che abbiamo discusso riguardo ai riferimenti:

  • In qualsiasi momento, è possibile avere o un riferimento mutabile o qualsiasi numero di riferimenti immutabili.
  • I riferimenti devono essere sempre validi.

Successivamente, esamineremo un tipo diverso di riferimento: slices.