Rc<T>, reference hisoblangan Smart Pointer

Ko‘p hollarda, ownership ochiq, ya’ni tushunarli holda bo’ladi: sizga qaysi o‘zgaruvchi qaysi berilgan qiymatga egalik qilishini aniq bilasiz. Ammo, ayrim hollarda bitta qiymat ko‘p egalarga ega bo‘lishi mumkin. Masalan, grafik ma’lumotlar tuzilmasida (data structures), bir nechta edgelar bitta nodeni point qilishi mumkin, vas shu node unga point qilingan barcha edgelar tomonidan egalik qilinadi. Nodega edgelar point qilinmagungacha, shuningdek egalari bo‘lmagungacha tozalanishi mumkin emas.

Rustning refenceni hisoblovchi Rc turini ishlatishda siz bir nechta ownershipni aniq qilib yoqishingiz kerak. Qiymatni ishlatilib turganligi yoki ishlamay turganligini aniqlash uchun Rc qiymatga ketayotgan refencelar sonini kuzatib boradi. Agar qiymatda nol (zero) referencelar bo‘lsa, hech qanay reference lar bekor bo‘lmasidan qiymat tozalanishi mumkin.

Rc<T> ni zaldagi televizor sifatida tasavvur qiling. Agar bir kishi televizor ko‘rish uchun xonaga kirsa, u televizorni yoqadi. Boshqalar esa shunchaki xonaga kirib tomosha qilsalar bo‘ladi. Xonadan oxirgi odam chiqib ketayotganda, ular televizorni o‘chirib ketishadi chunki televizor boshqa ishlatilmaydi. Agar bir kishi boshqalar televizorni tomosha qilib o‘tirganida o‘chirsa boshqalar uchun g‘alati bo‘lishi mumkin.

Biz Rc<T>ni ma’lumotni heapda dasturning ko‘p qismlarini o‘qishi uchun ajratishni hohlasaganimizda foydalanamiz va biz kompilyatsiya vaqti qaysi qism ma’lumotini oxirgi foydalanishni yakunlaganini bila olmaymiz. Agar biz ma’lumotni qaysi qismi oxirida to‘xtashini bilganimizda, biz shu qismni ma’lumotni egasi sifatida tayinlar edik va kompilyatsiya vaqtida qo‘llaniladigan oddiy egalik (ownership) qoidalari kuchga kirar edi.

Shuni yodda tutish kerakki Rc<T> yakka-thread holatlardagina ishlatiladi. Biz 16-bo‘limda parralellik haqida suhlashganimizda, biz ko‘p threadli dasturlarda referenceni hisoblashni qanday qilishni o‘rganamiz.

Rc<T>ni Ma'lumotni Ulashish uchun ishlatish

Keling kamchiligi bor bo‘lgan 15-5 ro‘yxat misolimizga qaytaylik. Esingizda bo‘lsa biz Box<T>ni ishlatishni ko‘rsatib o‘tgan edik. Bu safar, biz 2ta ro‘yxat ham egalikni (ownership) 3-ro‘yxat bilan ulashadigan ro‘yxat yaratamiz. Aniq qilib aytadigan bo‘lsak, 15-3 shaklga o‘xshashdir:

Two lists that share ownership of a third list

15-3-shakl: Ikkita ro'yxatlar, ya'ni b va c egalikni uchinchi ro'yxat, ya'ni aga ulashishi

Biz 5 va 10 dan iborat bo‘lgan a ro‘yxatni yaratamiz. Keyin yana ikkita ro‘yxatni ham yaratamiz, ya’ni 3dan boshlanadigan b va 4dan boshlanadigan c. b va c ro‘yxatlari 5 va 10dan iborat bo‘lgan a ro‘yxatda davom etadi. Boshqacha qilib aytganda, ro‘yxatlar birinchi 5 va 10dan iborat bo‘lgan birinchi ro‘yxat bilan ulashishadi.

15-17-ro‘yxatda ko‘rsatilgandek, bizning ssenariy bo‘yicha Box<T> bilan Listdagi ta’rifimiz yordamida implement qilishga urunsak ishga tushmaydi:

Fayl-nomi: 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));
}

15-17-ro'yxat: Ko'rib turganimizdek, Box<T> yordamida uchunchi ro'yxatga ikkita ro'yxatni egaligini (ownership) ulashib bo'lmaydi

Ushbu kodni kompilyatsiya qilsak, biz yuqoridagi xatolikni ko'ramiz:

$ 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

Cons variantlari o'zlariga tegishli bo'lgan ma'lumotlargagina egalik (own) qila oladi, shuninguchun biz b ro'yxatini yaratganimizda, a bga o'tadi va b aga egalik qiladi.Undan keyin, c ni yaratish uchun adan foydalanmoqchi bo'lganizmizda bizga ruxsat bermaydi chunki a ko'chib ketganligi uchun.

Buning o'rniga havolalarni ushlab turish uchun Cons ta'rifini o'zgartirishimiz mumkin, lekin keyin biz layvtaym parametrlarini ko'rsatishimiz kerak bo'ladi. Layvtaym parametrlarini belgilash orqali, biz ro'yxatdagi har bir elementning yashashini ko'rsatamiz. 15-17 ro'yxatda ko'rsatilganidek bu elementlar va ro'yxatlarga tegishli, lekin har doim gam emas.

15-18-ro'yxatda ko'rsatilganidek, Box<T>ning o'rnigaRc<T>ni ishlatish uchun biz bizning Ro'yxatimizning mazmunini o'zgartiramiz. Har bir Cons varianti qiymatni o'zida ushlab turadi va Rc<T> Ro'yxatni ko'rsatadi. aning egaligini olishning o'rniga bni yaratganimizda, a ni ushlab turgan Rc<List>ni klonlaymiz, shu bilan birga referenslar sonini birdan ikkiga ko'paytiramiz va Rc<List>dagi ma'lumotlarning egaligini ulashish uchun a va bga ruxsat beramiz. referenslar sonini ikkidan uchga ko'paytirgan holda, cni yaratayotganimizda ani ham klonlaymiz. Rc::cloneni har safar chaqirganimizda, Rc<List> tarkibidagi ma'lumotlarining referenslari soni oshiriladi, zero referenslar paydo bo'lmagungacha ma'lumotlar tozalanmaydi.

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

Listing 15-18: A definition of List that uses Rc<T>

Biz scopeni ichiga Rc<T>ni kiritsh uchun use statementini qo'shishimiz kerak chunki u muqaddimani ichida bo'lmagani uchun. mainni ichida 5 va 10ni saqlovch ro'yxatni yaratamiz va uni aga tegishli yangi Rc<List>ga joylashtiramiz. Keyin esa b va c yaratganimizda, Rc::clone funksiyasini chaqiramiz va argument sifatida aga tegishli bo'lgan Rc<List>ga o'tkazib yuboramiz.

Rc::clone(&a)ning o'rniga biz a.clone() chaqirishimiz mumkin edi, lekin Rust qoidalariga muofiq ushbu holatda Rc::clone ishlatgan ma'qul. Rc::clonening implementatsiyasi clonening ko'p implementatsiya turiga o'xshab ma'lumotlarni to'liq nusxa olmaydi. Rc::clonening chaqirilishi referenslarning sonini ko'paytiradi, va shuning uchun ham ko'p vaqt olmaydi. Ma'lumotlarni to'liq nusxalash esa ko'p vaqt talab etadi. Referenslarni hisoblash uchun Rc::cloneni ishlatsak, biz to'liq nusxalangan bilan referenslar soni ortgan nusxalar orasidagi farqni tasavvur qilishimiz mumkin. Kodning unumdorlik muammolarini qidirayotganimizda, bizga faqat to'liq nusxalangan nusxalarga e'tibor qaratib, Rc::clonening chaqirilishini e'tiborga olmasak ham bo'ladi.

Rc<T>ni nusxalash Referenslar hisobini orttiradi

ada referenslarni yaratib va uni Rc<List>ga drop qilib referenslar sonini ortayotganligini keling bizning ishlab turgan 15-18-ro'yxatimizdagi misolga o'zgartirish kiritib ko'raylik,

15-19-ro'yxatda, mainni o'zgartirib ko'raylik chunki uning ichki scope(doirasi) cro'yxatining atrofida shundan so'ng biz c scope(doirasida) chiqqanda qanday qilib referenslar soni ortayotganini ko'ramiz.

Fayl-nomi: 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));
}

15-19-ro'yxat: Referens soni ortayotganligini print qilish

Dasturda referens soni o'zgargan har bir nuqtada, Rc::strong_count funksiyasini chaqirish yordamida biz referens sonini print qilamiz. Ushbu funksiyani nomicountdeb emas strong_count deb nomlanadi, chunki Rc<T> turida weak_count ham bor; biz weak_countni "Referenslar siklini oldini olish: Rc<T>ni Weak<T>ga aylantirish" bo'limida ko'ramiz.

Ushbu kod quyidagini print qiladi:

$ 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

ada Rc<List>ning referens soni 1 ekanligini ko'rishimiz mumkin; keyinchalik har safar biz clone ni chaqirganimizda, son 1ga ortib boraveradi. c o'z scope(doirasi)dan chiqib ketganida,referens soni 1ga kamayadi. Rc::clone funksiyasi yordamida referens sonini ortirish uchun chaqirganimizdek referens soni kamaytirish uchun hech qanday funksiyasini chaqirishimiz kerak bo'lmaydi, chunki Drop traitining implementatsiyasi Rc<T> qiymati o'z scope(doirasi)dan chiqqanda avtomatik ravishda referens sonini kamaytiradi.

main oxirida b va keyin alarning scope(doirasidan) chiqib, son/hisob 0ga tenglashib, Rc<List> to'liq tozalanganligini biz ushbu misolda ko'ra olmaymiz. Rc<T>ni ishlatish yordamida bitta qiymat ko'p egalarga ega bo'lishi mumkin, hamda son/hisob qiymat egalaridan biri bor bo'lgunga qadar yaroqliligini tekshirib turadi.

O'zgarmas o'zgaruvchilar yordamida, Rc<T> o'qish uchun ma'lumotlarni dasturning ko'p joylari orasida ulashish imkonini beradi. Agar Rc<T> ko'p o'zgaruvchan referenslarga ega bo'lish imkonini bergan bo'lsa, siz 4-Bo'limda ko'rsatilganidek borrowing qoidalarini birini buzishingiz mumkin: ko'p o'zgaruchilar bir xil joyga borrow qilib data race va nomuvofiqliklarga sabab bo'lishi mumkin. Lekin ma'lumotni o'zrgatira olish foydalidir! keyingi bo'limda, biz ichki o'zgaruvchanlik shakli(pattern) va RefCell<T> turi bilan Rc<T> yordamida o'zgarmaslik cheklovi bilan ishlash ko'rib chiqamiz.