مقدمه

قوانین وام‌گیری Rust (borrowing rules) بسیار سخت‌گیرانه هستند: در هر لحظه، شما می‌توانید یا یک رفرنس تغییرپذیر (&mut T) داشته باشید یا هر تعداد رفرنس تغییرناپذیر (&T)، اما نه هر دو به صورت همزمان. این قانون در زمان کامپایل بررسی می‌شود. اما گاهی اوقات، ما به الگویی نیاز داریم که در آن یک مقدار تغییرناپذیر، بتواند مقدار داخلی خود را تغییر دهد. این الگو به نام «تغییرپذیری داخلی» یا Interior Mutability شناخته می‌شود.

Rust با ارائه نوع‌هایی مانند Cell<T> و RefCell<T>، این الگو را پیاده‌سازی می‌کند. این نوع‌ها به جای بررسی قوانین وام‌گیری در زمان کامپایل، آنها را در زمان اجرا بررسی می‌کنند. اگر قوانین نقض شوند (مثلاً تلاش برای گرفتن دو رفرنس تغییرپذیر به صورت همزمان)، برنامه به جای خطای کامپایل، panic خواهد کرد. این کار به ما اجازه می‌دهد تا در موارد خاصی که از ایمن بودن کد خود مطمئن هستیم، محدودیت‌های کامپایلر را دور بزنیم.

ترکیب Rc<T> و RefCell<T> برای مالکیت چندگانه و تغییرپذیری

در درس قبل دیدیم که Rc<T> به ما اجازه می‌دهد تا مالکیت چندگانه داشته باشیم، اما تنها به صورت تغییرناپذیر. حالا با ترکیب Rc<T> و RefCell<T>، می‌توانیم به یک الگوی بسیار قدرتمند دست پیدا کنیم: داشتن چندین مالک برای یک داده که همگی قادر به تغییر دادن آن هستند.

RefCell<T> مالک داده T است و قوانین وام‌گیری را در زمان اجرا بررسی می‌کند. این نوع دو متد اصلی دارد:

  • borrow(): یک اشاره‌گر هوشمند از نوع Ref<T> برمی‌گرداند که یک رفرنس تغییرناپذیر را نمایندگی می‌کند.
  • borrow_mut(): یک اشاره‌گر هوشمند از نوع RefMut<T> برمی‌گرداند که یک رفرنس تغییرپذیر را نمایندگی می‌کند.

RefCell<T> تعداد رفرنس‌های فعال (چه تغییرپذیر و چه تغییرناپذیر) را در زمان اجرا پیگیری می‌کند. اگر شما در حالی که یک رفرنس تغییرپذیر فعال دارید، سعی کنید رفرنس دیگری (چه mut و چه immut) بگیرید، برنامه panic خواهد کرد.

یک مثال کاربردی: لیست پیوندی با قابلیت تغییر

بیایید با استفاده از Rc<RefCell<T>>، لیست پیوندی از درس قبل را طوری بازنویسی کنیم که بتوانیم مقدار یک آیتم را پس از ایجاد لیست، تغییر دهیم.

Copy Icon src/main.rs
use std::{cell::RefCell, rc::Rc};

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

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

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    // Mutate the shared value
    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

در این کد، ما مقدار عددی هر گره را در داخل یک Rc<RefCell<i32>> قرار داده‌ایم. این به ما اجازه می‌دهد تا چندین مالک برای آن داشته باشیم و هر کدام از آنها بتوانند مقدار را تغییر دهند. در انتهای تابع main، ما با استفاده از value.borrow_mut() یک رفرنس تغییرپذیر به مقدار مشترک (که در ابتدا 5 بود) گرفته و آن را تغییر می‌دهیم. این تغییر در تمام لیست‌هایی که به این مقدار ارجاع دارند (a، b و c) منعکس خواهد شد.

در این درس با الگوی قدرتمند «تغییرپذیری داخلی» و نحوه پیاده‌سازی آن با استفاده از RefCell<T> آشنا شدیم. دیدیم که چگونه ترکیب Rc<T> و RefCell<T> به ما اجازه می‌دهد تا محدودیت‌های قوانین وام‌گیری را به صورت ایمن (با بررسی در زمان اجرا) دور زده و داده‌های قابل تغییر با چندین مالک داشته باشیم. اما این الگوها می‌توانند منجر به یک مشکل حافظه دیگر شوند. در درس بعدی، به بررسی «مصرف حافظه با چرخه‌های ارجاع» (Reference Cycles) و نحوه جلوگیری از آنها با استفاده از اشاره‌گرهای ضعیف خواهیم پرداخت.