Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rc<T>، اشاره‌گر (Pointer) هوشمند با شمارش مرجع

در بیشتر موارد، مالکیت واضح است: شما دقیقاً می‌دانید که کدام متغیر مالک یک مقدار مشخص است. با این حال، در مواردی ممکن است یک مقدار چندین مالک داشته باشد. برای مثال، در ساختارهای داده گراف، چندین یال ممکن است به یک گره اشاره کنند و آن گره از نظر مفهومی متعلق به تمام یال‌هایی است که به آن اشاره دارند. یک گره نباید پاکسازی شود مگر اینکه هیچ یالی به آن اشاره نکند و در نتیجه مالکیتی نداشته باشد.

برای فعال‌سازی مالکیت چندگانه باید به صورت صریح از نوع Rc<T> در Rust استفاده کنید که مخفف reference counting یا شمارش مرجع است. نوع Rc<T> تعداد مراجعات به یک مقدار را دنبال می‌کند تا مشخص کند که آیا آن مقدار هنوز در حال استفاده است یا خیر. اگر هیچ مرجعی به یک مقدار وجود نداشته باشد، مقدار می‌تواند بدون اینکه هیچ مرجعی نامعتبر شود، پاکسازی شود.

تصور کنید Rc<T> مانند یک تلویزیون در اتاق نشیمن است. وقتی یک نفر وارد اتاق می‌شود تا تلویزیون تماشا کند، آن را روشن می‌کند. افراد دیگری هم می‌توانند وارد اتاق شوند و تلویزیون تماشا کنند. وقتی آخرین نفر اتاق را ترک می‌کند، تلویزیون را خاموش می‌کند زیرا دیگر استفاده نمی‌شود. اگر کسی تلویزیون را در حالی که دیگران هنوز در حال تماشای آن هستند خاموش کند، اعتراض تماشاگران باقی‌مانده بلند خواهد شد!

ما از نوع Rc<T> استفاده می‌کنیم وقتی می‌خواهیم مقداری را در هیپ تخصیص دهیم که توسط چندین بخش از برنامه ما خوانده شود و نمی‌توانیم در زمان کامپایل تعیین کنیم که کدام بخش استفاده از داده را زودتر به پایان می‌رساند. اگر می‌دانستیم کدام بخش زودتر تمام می‌شود، می‌توانستیم آن بخش را مالک داده کنیم و قوانین معمول مالکیت که در زمان کامپایل اعمال می‌شود، اعمال می‌شد.

توجه داشته باشید که Rc<T> فقط برای استفاده در سناریوهای تک‌ریسمانی است. هنگامی که در فصل 16 در مورد هم‌زمانی بحث می‌کنیم، نحوه انجام شمارش مرجع در برنامه‌های چندریسمانی را پوشش خواهیم داد.

استفاده از Rc<T> برای اشتراک‌گذاری داده

بیایید به مثال لیست cons که در لیست 15-5 دیدیم برگردیم. به خاطر دارید که این لیست را با استفاده از Box<T> تعریف کرده بودیم. این بار، قصد داریم دو لیست ایجاد کنیم که هر دو مالکیت مشترکی از یک لیست سوم داشته باشند. از نظر مفهومی، این ساختار شبیه به تصویر 15-3 است.

A linked list with the label 'a' pointing to three elements: the first element contains the integer 5 and points to the second element. The second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the list; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element of list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a', so that the tail of lists 'b' and 'c' are both list 'a'

شکل 15-3: دو لیست، b و c، که مالکیت یک لیست سوم، a را به اشتراک می‌گذارند

ما ابتدا لیستی به نام a ایجاد می‌کنیم که شامل مقادیر 5 و سپس 10 است. سپس دو لیست دیگر می‌سازیم: لیست b که با مقدار 3 شروع می‌شود و لیست c که با مقدار 4 شروع می‌شود. هر دو لیست b و c در ادامه به لیست a که شامل 5 و 10 است اشاره خواهند کرد. به عبارت دیگر، هر دو لیست b و c لیست a را به صورت مشترک استفاده می‌کنند.

تلاش برای پیاده‌سازی این سناریو با استفاده از تعریف فعلی List که از Box<T> استفاده می‌کند، امکان‌پذیر نیست؛ همان‌طور که در لیست 15-17 نشان داده شده است.

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

هنگامی که این کد را کامپایل می‌کنیم، با این خطا مواجه می‌شویم:

$ 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` (bin "cons-list") due to 1 previous error

متغیرهای Cons مالک داده‌هایی هستند که در خود نگه می‌دارند. بنابراین، هنگامی که لیست b را ایجاد می‌کنیم، a به b منتقل می‌شود و b مالک a می‌شود. سپس، هنگامی که سعی می‌کنیم دوباره از a برای ایجاد c استفاده کنیم، این کار مجاز نیست زیرا a قبلاً منتقل شده است.

ما می‌توانیم تعریف Cons را به گونه‌ای تغییر دهیم که به جای نگهداری داده‌ها، ارجاع به آنها را نگه دارد. اما در این صورت باید پارامترهای طول عمر (lifetime parameters) را مشخص کنیم. با مشخص کردن پارامترهای طول عمر، مشخص می‌کنیم که هر عنصر در لیست حداقل به اندازه کل لیست زنده خواهد بود. این موضوع در مورد عناصر و لیست‌های موجود در لیست 15-17 صدق می‌کند، اما در همه سناریوها چنین نیست.

در عوض، تعریف List خود را تغییر می‌دهیم تا به جای Box<T> از Rc<T> استفاده کند، همان‌طور که در لیست 15-18 نشان داده شده است. هر متغیر Cons اکنون یک مقدار و یک Rc<T> اشاره‌کننده به یک List را نگه می‌دارد. وقتی b را ایجاد می‌کنیم، به جای تصاحب مالکیت a، Rc<List> که a نگه می‌دارد را کلون می‌کنیم، بنابراین تعداد ارجاعات از یک به دو افزایش می‌یابد و به a و b اجازه می‌دهیم مالکیت داده‌های موجود در آن Rc<List> را به اشتراک بگذارند. همچنین هنگام ایجاد c، a را کلون می‌کنیم و تعداد ارجاعات از دو به سه افزایش می‌یابد. هر بار که Rc::clone را فراخوانی می‌کنیم، تعداد ارجاعات به داده‌های موجود در Rc<List> افزایش می‌یابد و داده‌ها تا زمانی که هیچ ارجاعی به آنها باقی نماند پاک نمی‌شوند.

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: تعریفی از List که از Rc<T> استفاده می‌کند

ما نیاز داریم یک دستور use اضافه کنیم تا Rc<T> را وارد دامنه کنیم، زیرا این نوع در prelude قرار ندارد. در تابع main، یک لیست که مقادیر 5 و 10 را نگه می‌دارد ایجاد می‌کنیم و آن را در یک Rc<List> جدید به نام a ذخیره می‌کنیم. سپس، زمانی که b و c را می‌سازیم، تابع Rc::clone را فراخوانی می‌کنیم و یک رفرنس به Rc<List> موجود در a را به عنوان آرگومان به آن پاس می‌دهیم.

می‌توانستیم a.clone() را به جای Rc::clone(&a) فراخوانی کنیم، اما طبق قرارداد Rust در این موارد از Rc::clone استفاده می‌شود. پیاده‌سازی Rc::clone یک کپی عمیق از تمام داده‌ها ایجاد نمی‌کند، همان‌طور که پیاده‌سازی اکثر انواع دیگر clone این کار را انجام می‌دهد. فراخوانی Rc::clone فقط تعداد ارجاعات را افزایش می‌دهد، که زمان زیادی نمی‌برد. کپی عمیق داده‌ها ممکن است زمان زیادی ببرد. با استفاده از Rc::clone برای شمارش مرجع، می‌توانیم بین کپی‌های عمیق و کپی‌هایی که تعداد ارجاعات را افزایش می‌دهند تمایز بصری قائل شویم. هنگام جستجوی مشکلات عملکرد در کد، فقط لازم است به کپی‌های عمیق توجه کنیم و می‌توانیم فراخوانی‌های Rc::clone را نادیده بگیریم.

کلون کردن یک Rc<T> تعداد ارجاعات را افزایش می‌دهد

اجازه دهید مثال کاری خود را در لیست 15-18 تغییر دهیم تا بتوانیم تغییرات تعداد ارجاعات را هنگام ایجاد و حذف ارجاعات به Rc<List> در a مشاهده کنیم.

در لیست 15-19، main را تغییر خواهیم داد تا یک محدوده داخلی (inner scope) در اطراف لیست c داشته باشد؛ سپس می‌توانیم ببینیم که چگونه تعداد ارجاعات زمانی که c از محدوده خارج می‌شود تغییر می‌کند.

Filename: src/main.rs
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

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

// --snip--

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));
}
Listing 15-19: چاپ تعداد ارجاعات

در هر نقطه از برنامه که تعداد ارجاعات تغییر می‌کند، تعداد ارجاعات را چاپ می‌کنیم که از طریق فراخوانی تابع Rc::strong_count دریافت می‌شود. این تابع به جای count، strong_count نام‌گذاری شده است زیرا نوع Rc<T> همچنین دارای weak_count است؛ در بخش “جلوگیری از چرخه‌های مرجع: تبدیل یک Rc<T> به یک Weak<T> با کاربرد weak_count آشنا خواهیم شد.

این کد خروجی زیر را تولید می‌کند:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [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

می‌بینیم که Rc<List> در a تعداد ارجاع اولیه برابر با 1 دارد؛ سپس هر بار که clone را فراخوانی می‌کنیم، تعداد ارجاعات 1 واحد افزایش می‌یابد. هنگامی که c از محدوده خارج می‌شود، تعداد ارجاعات 1 واحد کاهش می‌یابد. لازم نیست تابعی برای کاهش تعداد ارجاعات فراخوانی کنیم، همان‌طور که باید Rc::clone را برای افزایش تعداد ارجاعات فراخوانی کنیم: پیاده‌سازی ویژگی Drop تعداد ارجاعات را به طور خودکار کاهش می‌دهد وقتی که یک مقدار Rc<T> از محدوده خارج می‌شود.

آنچه در این مثال نمی‌توانیم ببینیم این است که وقتی b و سپس a در انتهای main از محدوده خارج می‌شوند، تعداد ارجاعات به 0 می‌رسد و Rc<List> به طور کامل پاک‌سازی می‌شود. استفاده از Rc<T> به یک مقدار اجازه می‌دهد که چندین مالک داشته باشد، و تعداد ارجاعات تضمین می‌کند که مقدار تا زمانی که هر یک از مالکان هنوز وجود دارند، معتبر باقی می‌ماند.

از طریق ارجاعات غیرقابل تغییر، Rc<T> به شما اجازه می‌دهد داده‌ها را بین بخش‌های مختلف برنامه خود برای خواندن به اشتراک بگذارید. اگر Rc<T> به شما اجازه می‌داد که چندین ارجاع قابل تغییر نیز داشته باشید، ممکن بود یکی از قوانین قرض‌گیری که در فصل 4 بحث شد را نقض کنید: چندین قرض قابل تغییر به یک مکان می‌تواند باعث ایجاد تناقضات و مسابقه داده‌ها شود. اما توانایی تغییر داده‌ها بسیار مفید است! در بخش بعدی، به الگوی تغییر‌پذیری داخلی (interior mutability) و نوع RefCell<T> که می‌توانید همراه با Rc<T> برای کار با این محدودیت عدم تغییر‌پذیری استفاده کنید، خواهیم پرداخت.