مقدمه

زبان Rust به دلیل تضمین‌های ایمنی حافظه‌ای که در زمان کامپایل ارائه می‌دهد، شهرت دارد. سیستم مالکیت، وام‌گیری و lifetimeها از بروز بسیاری از باگ‌های رایج مربوط به حافظه جلوگیری می‌کنند. با این حال، یک نوع مشکل حافظه وجود دارد که حتی در Rust نیز ممکن است رخ دهد: «نشت حافظه» یا Memory Leak. این اتفاق زمانی می‌افتد که حافظه تخصیص داده می‌شود اما هرگز آزاد نمی‌شود و برای برنامه غیرقابل استفاده باقی می‌ماند.

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

ایجاد یک چرخه ارجاع

یک چرخه ارجاع زمانی اتفاق می‌افتد که دو یا چند آیتم به گونه‌ای به یکدیگر ارجاع دهند که یک حلقه بسته ایجاد شود. در این حالت، شمارنده ارجاع (reference count) هر آیتم در حلقه هرگز به صفر نمی‌رسد، حتی اگر هیچ متغیر خارجی دیگری به آنها دسترسی نداشته باشد. در نتیجه، حافظه آنها هرگز drop و آزاد نمی‌شود.

بیایید این مشکل را با یک مثال از لیست پیوندی که یک عضو آن به خودش اشاره می‌کند، ببینیم.

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

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

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
    println!("a initial rc count = {}", Rc::strong_count(&a));

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
    println!("a rc count after b creation = {}", Rc::strong_count(&a));

    // Create the reference cycle
    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("a rc count after changing a = {}", Rc::strong_count(&a));
    println!("b rc count after changing a = {}", Rc::strong_count(&b));

    // Uncommenting the next line will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

در این کد، ما دو لیست a و b می‌سازیم که در آن، b به a اشاره می‌کند. سپس، ما دم لیست a را طوری تغییر می‌دهیم که به b اشاره کند. حالا a به b و b به a اشاره می‌کند و یک چرخه ایجاد شده است. شمارنده ارجاع (strong count) هر دو Rc برابر با ۲ می‌شود. وقتی main به پایان می‌رسد، Rust سعی می‌کند b را drop کند که شمارنده a را به ۱ کاهش می‌دهد. سپس سعی می‌کند a را drop کند که شمارنده b را به ۱ کاهش می‌دهد. شمارنده هیچ‌کدام هرگز به صفر نمی‌رسد و حافظه آنها نشت می‌کند.

جلوگیری از چرخه‌های ارجاع با Weak<T>

برای حل این مشکل، Rust یک نوع دیگر از اشاره‌گر هوشمند به نام Weak<T> را ارائه می‌دهد. Weak<T> یک «رفرنس ضعیف» به یک مقدار است. برخلاف Rc<T>، یک رفرنس ضعیف از drop شدن مقداری که به آن اشاره می‌کند، جلوگیری نمی‌کند.

برای ایجاد یک رفرنس ضعیف، از تابع Rc::downgrade() روی یک Rc<T> استفاده می‌کنیم. این تابع یک اشاره‌گر هوشمند از نوع Weak<T> برمی‌گرداند. برای دسترسی به مقدار داخل یک Weak<T>، باید با استفاده از متد upgrade() آن را به یک Option<Rc<T>> تبدیل کنیم. اگر مقدار هنوز drop نشده باشد، Some(Rc<T>) و در غیر این صورت None را برمی‌گرداند. این الگو تضمین می‌کند که Rust هرگز یک رفرنس آویزان به شما نخواهد داد.

با تغییر ساختار داده‌ی خود برای استفاده از Weak<T> در جایی که چرخه ممکن است رخ دهد، می‌توانیم از نشت حافظه جلوگیری کنیم.

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