Referințe și procesul de împrumutare

Dificultatea pe care o întâmpinăm cu codul pentru tuplă, din Listarea 4-5, este că trebuie să returnăm String-ul către funcția apelantă pentru a putea utiliza în continuare String-ul după apelul funcției calculate_length. Aceasta se datorează faptului că String-ul a fost permutat în interiorul funcției calculate_length. O soluție alternativă ar fi să furnizăm o referință la valoarea String-ului. O referință funcționează similar unui pointer - ea reprezintă o adresă ce permite accesul la datele stocate la acea adresă; aceste date fiind deținute de o altă variabilă. Însă, spre deosebire de un pointer, o referință este garantată să indice o valoare validă a unui anumit tip, pe toată durata vieții acestei referințe.

Iată cum definim și utilizăm funcția calculate_length care are ca parametru o referință la un obiect, în loc să preia posesiunea valorii:

Numele fișierului: 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()
}

În primul rând, observăm că tot codul referitor la tuplă, din declarația variabilei și valoarea de return a funcției, a dispărut. În al doilea rând, remarcăm faptul că transmitem &s1 către funcția calculate_length și că, în definiția sa, preluăm &String și nu String. Aceste semne de ampersand reprezintă referințe și permit să te referi la o anumită valoare fără a-i prelua posesiunea. Conceptul este reprezentat în Figura 4-5.

Trei tabele: tabelul destinat lui s conține doar un pointer către tabelul
asociat lui s1. Tabelul pentru s1 include datele din stivă pentru s1 și direcționează
spre datele de tip string stocate pe heap.

Figura 4-5: Diagrama ilustrând &String s care direcționează spre String s1

Notă: Procesul invers referențierii prin utilizarea & este dereferențierea, realizată cu ajutorul operatorului de dereferențiere, *. Vom explora câteva situații de utilizare a operatorului de dereferențiere în Capitolul 8 și vom discuta în detaliu despre dereferențiere în Capitolul 15.

Să examinăm mai în detaliu apelul funcției de aici:

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

Sintaxa &s1 ne permite să creăm o referință care indică valoarea s1 însă fără a o deține. Dat fiind faptul că nu o deține, valoarea la care face referire nu va fi abandonată (dropped) când referința nu mai este folosită.

În mod similar, semnătura funcției folosește & pentru a indica faptul că tipul parametrului s este o referință. Să adăugăm câteva adnotări care să clarifice aceste aspecte:

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.

Domeniul în care este validă variabila s coincide cu cel al oricărui parametru de funcție. Cu toate acestea, valoarea către care se referă nu este eliminată când nu mai folosim s. Motivul este simplu: s nu are posesiunea acestei valori. Dacă funcțiile noastre folosesc referințe ca parametri, în locul valorilor propriu-zise, nu trebuie să returnăm valorile pentru a restitui posesiunea, deoarece, de fapt, nu am avut niciodată posesiunea acestora.

Acest act de a crea o referință îl numim împrumutare. În mod similar cu viața de zi cu zi, dacă o persoană deține ceva, tu poți să îi împrumuți acel lucru. Odată ce ai terminat cu acel lucru, trebuie să îl restitui. Întrucât nu îți aparține.

Așadar, ce se întâmplă dacă încercăm să modificăm ceva ce doar împrumutăm? Încearcă codul din Listarea 4-6. Dar te previn: nu o să iasă exact cum te-ai așteptat!

Numele fișierului: src/main.rs

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

    change(&s);
}

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

Listarea 4-6: Tentativa de a modifica o valoare împrumutată

Aceasta este eroarea întâlnită:

$ 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

Exact așa cum sunt imutabile variabilele, în mod implicit, la fel sunt și referințele. Nu ne este permisă modificarea unei valori la care deținem doar o referință.

Referințe mutabile

Putem corecta codul din Listarea 4-6 pentru a ne oferi posibilitatea de a modifica o valoare împrumutată, prin câteva ajustări minore care implică utilizarea unei referințe mutabile:

Numele fișierului: 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");
}

În primul rând, modificăm s pentru a fi mut. Apoi, creăm o referință mutabilă cu &mut s în momentul în care invocăm funcția change, și actualizăm semnătura funcției pentru a accepta o referință mutabilă cu some_string: &mut String. Astfel, devine foarte clar că funcția change va modifica valoarea pe care o împrumută.

Referințele mutabile vin însă cu o limitare importantă: dacă deții o referință mutabilă la o valoare, nu poți deține alte referințe către aceeași valoare. Acest cod, care încearcă să creeze două referințe mutabile la s, va da eroare:

Filename: src/main.rs

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

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

Și eroarea:

$ 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

Această eroare ne informează că respectivul cod nu este valid, întrucât nu se poate împrumuta s în mod mutabil de mai multe ori concomitent. Primul împrumut mutabil se realizează în r1 și trebuie să continue până când este folosit în instrucțiunea println!. Cu toate acestea, între momentul creării acestei referințe mutabile și momentul utilizării sale, am încercat să generăm o altă referință mutabilă în r2, care împrumută aceleași date ca și r1.

Restricția care interzice mai multe referințe mutabile simultane la aceleași date ne permite să modificăm datele, dar într-un mod extrem de controlat. Aceasta este o provocare des întâlnită pentru cei nou-veniți în lumea Rust, deoarece majoritatea limbajelor de programare permit modificarea datelor oricând. Avantajul acestei restricții este faptul că Rust poate preveni apariția curselor de date în timpul compilării. O cursă de date este similară cu o condiție de cursă și se produce atunci când au loc următoarele trei evenimente:

  • Două sau mai multe pointere accesează simultan aceleași date.
  • Cel puțin un pointer este utilizat pentru a scrie date.
  • Nu există niciun mecanism în vigoare care să sincronizeze accesul la date.

Cursele de date creează un comportament nedefinit și pot fi dificile de diagnosticat și remediat atunci când încerci să le detectezi în timpul execuției; Rust preîntâmpină acest gen de problemă prin faptul că refuză să compileze codul în care apar curse de date!

Ca de obicei, avem posibilitatea de a utiliza acolade pentru a iniția un nou domeniu de vizibilitate, astfel facilitând existența mai multor referințe mutabile; singura condiție este aceea de a nu fi simultane:

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 impune o regulă similară pentru combinarea referințelor mutabile și imutabile. Acest cod generează o eroare:

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

Eroarea:

$ 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

De asemenea, nu ne este permis să avem o referință mutabilă în același timp cu o referință imutabilă către aceeași valoare.

Cei ce folosesc o referință imutabilă nu se așteaptă ca valoarea să se schimbe brusc, fără un avertisment! Totuși, sunt permise multiple referințe imutabile, deoarece nicio persoană ce doar citește datele nu are abilitatea de a interfera cu lectura altcuiva a acelor date.

Trebuie să reții că domeniul de vizibilitate al unei referințe începe de unde este aceasta introdusă și continuă până la ultima utilizare a respectivei referințe. De pildă, acest cod se va compila deoarece ultima folosire a referințelor imutabile, și anume println!, are loc înainte de a fi introdusă referința mutabilă:

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

Domeniile de vizibilitate pentru referințele imutabile r1 și r2 se încheie imediat după ce au fost utilizate pentru ultima dată în println!. Asta se întâmplă înainte ca referința mutabilă r3 să fie creată. Aceste domenii nu se intersectează, deci nu interferă unul cu celălalt, făcând acest cod valid. Compilatorul Rust este în stare să determine că referința nu mai este folosită înainte de sfârșitul domeniului de vizibilitate.

Știm că erorile legate de împrumutări pot fi frustrante uneori. Totuși, trebuie să înțelegem că acesta este un mecanism prin care compilatorul Rust ne avertizează asupra unui potențial bug în stadiul de compilare, nu în timpul rulării. În plus, ne indică exact locul unde apare problema. Aceasta îți va economisi timp deoarece nu va trebui să cauți motivul pentru care datele tale nu corespund cu ceea ce te așteptai.

Referințe fără destinație

În limbajele de programare ce utilizează pointeri, este foarte ușor să generăm, chiar și involuntar, un pointer în aer (dangling) - un pointer ce își pierde legătura cu o parte de memorie care poate fi atribuită altei operațiuni - prin simpla eliberare a unei părți din memoria alocate, în timp ce pointerul către acea memorie rămâne încă activ. În contrast, Rust, prin intermediul compilatorului, garantează că niciodată nu vor exista referințe fără destinație: dacă avem o referință la niște date, compilatorul se va asigura că acele date nu vor fi scoase din domeniul de vizibilitate înainte ca referința spre ele să o facă.

Acum, să încercăm să generăm o referință fără destinație, doar pentru a observa cum Rust previne acest lucru printr-o eroare la etapa de compilare:

Numele fișierului: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

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

    &s
}

Vom primi eroarea:

$ 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

Acest mesaj de eroare face referire la un concept pe care încă nu l-am abordat: durate de viață. Vom aprofunda această temă în Capitolul 10. Cu toate acestea, chiar dacă nu ținem cont de porțiunile ce se referă la durate de viață, mesajul (tradus) ne oferă o pistă esențială pentru a intelege de ce acest fragment de cod nu funcționează:

tipul de retur al acestei funcții conține o valoare împrumutată, dar nu există nicio valoare pentru a fi împrumutată

Să examinăm mai atent ce se petrece la fiecare pas în codul nostru dangle:

Numele fișierului: 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!

Dat fiind că s este generat în interiorul dangle, la finalizarea rulării codului din dangle, s va fi dezalocat. Cu toate acestea, noi am încercat să returnăm o referință către s. Aceasta presupune că referința noastră ar fi îndreptată către un String ce devine invalid. Situația nu este deloc favorabilă! Rust nu ne va permite să desfășurăm o astfel de acțiune.

Soluția în acest caz constă în returnarea directă a String:

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

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

    s
}

Aceasta funcționează fără nici o problemă. Posesiunea este permutată și nimic nu este dezalocat.

Regulamentul referințelor

Să reamintim principiile pe care le-am discutat despre referințe:

  • În orice clipă, ai voie să deții ori o singură referință mutabilă sau o cantitate nelimitată de referințe imutabile.
  • Referințele trebuie să fie valide în permanență.

Următoarea etapă va fi examinarea unui alt tip de referință: slice-urile.