Rc<T>, pointerul inteligent cu numărare a referințelor

În mare parte din cazuri, conceptul de posesiune este destul de limpede: este evident care variabilă deține o anumită valoare. Cu toate acestea, există situații când o valoare poate fi proprietatea mai multor deținători. Spre exemplu, în structuri de date grafice, multiple muchii pot să indice spre același nod, care este conceptual în posesiunea tuturor muchiilor care îl indică. Acest nod nu ar trebui eliminat până nu rămâne fără nicio muchie care să-l indice, adică fără proprietari.

Posesiunea multiplă trebuie activată în mod intenționat prin utilizarea tipului Rust Rc<T>, acronim pentru reference counted (numărarea referințelor). Tipul Rc<T> urmărește numărul de referințe către o valoare pentru a determina dacă aceasta mai este în uz. Dacă nu există nicio referință către o valoare, aceasta poate fi eliminată fără ca vreo referință să devină nevalidă.

Gândește-te la Rc<T> ca la un televizor în sufragerie. Când cineva intră să vizioneze televizorul, îl pornește. Alți membri pot intra și ei și se pot bucura de program. Când ultimul telespectator părăsește sufrageria, oprește televizorul pentru că nu se mai folosește. Dacă televizorul ar fi întrerupt în timp ce alții încă se uită, ar crea nemulțumire în rândul celor rămași!

Tipul Rc<T> este indicat atunci când vrem să plasăm niște date pe heap pentru a fi accesate de diferite părți ale programului nostru și când nu putem să determinăm la momentul compilării care parte va finaliza utilizarea datelor cel din urmă. Dacă am cunoaște care fragment ar termina în ultimul rând, i-am putea da posesiune asupra datelor și regulile normale de posesiune ar fi aplicate de compilator.

Ține minte că Rc<T> este destinat utilizării în contexte cu un singur fir de execuție (single-threaded). Când vom aborda tema concurenței în Capitolul 16, vom descrie modalitățile de a implementa numărarea referințelor în programele cu executare paralelă (multithread).

Utilizarea Rc<T> pentru partajarea date

Să revenim la exemplul nostru cu lista de tip cons prezentat în Listarea 15-5. Vă amintim că am definit-o utilizând Box<T>. Acum, vom crea două liste care dețin împreună posesiunea unei a treia liste. Conceptual, acest lucru este similar cu Figura 15-3:

Două liste care dețin împreună posesiunea unei a treia liste

Figura 15-3: Două liste, b și c, care dețin împreună posesiunea unei a treia liste, a

Vom construi lista a, care cuprinde numerele 5 și apoi 10. Apoi, vom crea alte două liste: lista b, care pornește cu numărul 3, și lista c, care începe cu 4. Lista b și lista c se vor extinde apoi cu lista a, care include 5 și 10, astfel încât ambele liste vor împărtăși lista a.

Dacă încercăm să implementăm acest scenariu cu definiția noastră a List care folosește Box<T>, descoperim că nu funcționează, așa cum este demonstrat în Listarea 15-17:

Numele fișierului: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Listarea 15-17: Demonstrând că nu ne este permis să avem două liste utilizând Box<T> care să încerce să dețină împreună posesiunea unei a treia liste

La compilarea acestui cod, întâmpinăm următoarea eroare:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error

Variantelor Cons le aparține în mod exclusiv datele pe care le conțin, așadar atunci când creăm lista b, instanța a este permutată în b și devine posesiunea listei b. Când încercăm să folosim a din nou pentru a crea lista c, nu se poate, deoarece a a fost deja permutată.

În locul acestui lucru, am putea considera modificarea lui Cons pentru a include referințe, însă acest demers ar necesita definitivarea unor parametri ai duratei de viață. Aceasta ar implica că fiecare element al listei trebuie să existe pentru cel puțin tot atât timp cât există lista în totalitate. Acesta este cazul pentru elementele și listele din Listarea 15-17, dar nu este o regulă general valabilă.

În schimb, vom modifica definiția lui List pentru a utiliza Rc<T> în locul lui Box<T>, după cum este ilustrat în Listarea 15-18. Acum, fiecare variantă Cons va conține o valoare și un Rc<T> ce indică spre o listă List. Când realizăm b, nu vom prelua controlul asupra lui a, ci vom clona Rc<List> pe care a îl menține, crescând astfel numărul de referințe de la unu la doi și permițând lui a și b să partajeze posesiunea datelor în respectivul Rc<List>. Vom proceda similar cu clonarea lui a când construim c, mărind numărul de referințe de la doi la trei. La fiecare apel al lui Rc::clone, contorul de referințe la datele din Rc<List> va crește, iar datele nu vor fi eliminate decât când nu vor mai exista referințe către acestea.

Numele fișierului: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Listarea 15-18: Definiția lui List folosind Rc<T>

Este necesar să adăugăm o declarație use pentru a importa Rc<T> în domeniul nostru de vizibilitate, deoarece nu este inclus în mod implicit în preludiul limbajului Rust. În funcția main, cream lista ce conține valorile 5 și 10 și o salvăm într-un Rc<List> nou în variabila a. Apoi, la crearea variabilelor b și c, invocăm funcția Rc::clone, oferind ca argument o referință către Rc<List> din a.

Ar fi fost posibil să folosim a.clone() în loc de Rc::clone(&a), dar convenția în Rust este de a folosi Rc::clone în această situație. Implementarea lui Rc::clone nu realizează o copie completă a tuturor datelor, așa cum se întâmplă la cele mai multe implementări ale funcției clone pentru alte tipuri de date. Apelul funcției Rc::clone se limitează la incrementarea contorului de referințe, proces ce nu durează mult. Realizarea de copii complete ale datelor poate fi un proces care să consume timp semnificativ. Utilizând Rc::clone în scopul numărării referințelor, putem face o distincție clară între clonările ce presupun copii complete ale datelor și cele care doar cresc numărul de referințe. Astfel, la căutarea problemelor de performanță în cod, ne vom limita analiza doar la primele și vom ignora apelurile la Rc::clone.

Clonarea unui Rc<T> mărește contorul de referințe

Să ajustăm exemplul nostru actual din Listarea 15-18 pentru a observa cum variază numărul de referințe atunci când sunt create și eliminate referințe către Rc<List> din a.

În Listarea 15-19, vom modifica main pentru a introduce un domeniu de vizibilitate intern în jurul listei c. Astfel, vom putea vedea cum se modifică contorul de referințe odată ce c nu mai este în domeniu.

Numele fișierului: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Listarea 15-19: Afișarea contorului de referințe

În fiecare punct al programului unde contorul de referințe se modifică, afișăm contorul de referințe, obținut prin apelarea funcției Rc::strong_count. Această funcție poartă numele de strong_count în loc de count deoarece tipul Rc<T> include și un weak_count; vom vedea utilizarea weak_count în secțiunea „Evitarea ciclurilor de referințe: Transformarea unui Rc<T> într-un Weak<T>.

Acest cod va afișa următoarele:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Observăm că Rc<List> în a pornește cu un contor de referințe de 1; apoi, de fiecare dată când invocăm clone, contorul crește cu 1. Când c nu mai este în scop, contorul scade cu 1. Nu avem nevoie să apelăm o funcție specială pentru a reduce contorul de referințe, așa cum facem cu Rc::clone pentru a-l crește: implementarea trăsăturii Drop diminuează automat contorul de referințe atunci când o valoare Rc<T> părăsește domeniul de vizibilitate.

Ce nu se vede în acest exemplu este faptul că, atunci când b și apoi a nu mai sunt în scop la sfârșitul funcției main, contorul ajunge la 0, iar Rc<List> este eliberat complet. Utilizarea Rc<T> permite ca o valoare unică să aparțină mai multor posesori, contorul asigurând că valoarea rămâne validă atâta timp cât există cel puțin unul din ei.

Printr-o serie de referințe imutabile, Rc<T> ne permite să partajăm date între diverse componente ale programului nostru, limitându-ne doar la citire. Dacă Rc<T> ar oferi posibilitatea de a avea de asemenea referințe mutabile multiple, s-ar putea încălca una dintre regulile de împrumut menționate în Capitolul 4: referințe multiple mutabile către același obiect pot declanșa curse de date și inconsistențe. Totuși, abilitatea de a modifica datele este de mare ajutor! În secțiunea următoare, vom discuta modelul de mutabilitate internă și tipul RefCell<T>, pe care îl poți utiliza împreună cu Rc<T> pentru a gestiona această limitare a imutabilității.