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> و RefCell<T>: امکان ایجاد ارجاع‌هایی وجود دارد که آیتم‌ها در آن به یکدیگر در یک چرخه ارجاع می‌دهند. این باعث نشت حافظه می‌شود، زیرا شمارش ارجاع هر آیتم در چرخه هرگز به 0 نمی‌رسد و مقادیر هرگز حذف نمی‌شوند.

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

بیایید بررسی کنیم که چگونه ممکن است یک چرخه‌ی رفرنس (reference cycle) به‌وجود بیاید و چگونه می‌توان از آن جلوگیری کرد. این بررسی را با تعریف enumی به نام List و متدی به نام tail در لیستینگ 15-25 آغاز می‌کنیم.

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

#[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() {}
Listing 15-25: تعریف یک لیست cons که یک RefCell<T> نگه می‌دارد تا بتوانیم آنچه که یک متغیر Cons به آن اشاره می‌کند را تغییر دهیم

ما از یک نسخه دیگر از تعریف List که در فهرست 15-5 آمده بود استفاده می‌کنیم. عنصر دوم در متغیر Cons اکنون RefCell<Rc<List>> است، به این معنی که به جای توانایی تغییر مقدار i32 که در فهرست 15-24 داشتیم، می‌خواهیم مقدار List را که یک متغیر Cons به آن اشاره می‌کند، تغییر دهیم. همچنین، یک متد tail اضافه می‌کنیم تا دسترسی به آیتم دوم را در صورتی که یک متغیر Cons داریم، راحت‌تر کنیم.

در فهرست 15-26، یک تابع main اضافه می‌کنیم که از تعاریف فهرست 15-25 استفاده می‌کند. این کد لیستی در a و لیستی در b ایجاد می‌کند که به لیست a اشاره می‌کند. سپس لیست در a را تغییر می‌دهد تا به b اشاره کند و یک چرخه ارجاعی ایجاد کند. در طول این فرآیند، اظهارات println! وجود دارند که نشان می‌دهند شمارش ارجاع در نقاط مختلف چه مقدار است.

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

#[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));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

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

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

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack.
    // println!("a next item = {:?}", a.tail());
}
Listing 15-26: ایجاد یک چرخه ارجاعی از دو مقدار List که به یکدیگر اشاره می‌کنند

ما یک نمونه‌ی Rc<List> ایجاد می‌کنیم که یک مقدار از نوع List را در متغیر a نگه می‌دارد و لیست اولیه‌ای به شکل 5, Nil دارد. سپس یک نمونه‌ی دیگر از Rc<List> در متغیر b می‌سازیم که مقداری برابر با 10 دارد و به لیست موجود در a اشاره می‌کند.

ما a را تغییر می‌دهیم تا به جای Nil به b اشاره کند، و یک چرخه ایجاد می‌کنیم. این کار را با استفاده از متد tail انجام می‌دهیم تا یک ارجاع به RefCell<Rc<List>> در a بگیریم، که آن را در متغیر link قرار می‌دهیم. سپس از متد borrow_mut روی RefCell<Rc<List>> استفاده می‌کنیم تا مقدار داخلی را از یک Rc<List> که مقدار Nil را نگه می‌دارد به Rc<List> در b تغییر دهیم.

وقتی این کد را اجرا می‌کنیم و println! آخر را به طور موقت کامنت می‌کنیم، خروجی زیر را دریافت می‌کنیم:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

تعداد رفرنس‌های نمونه‌های Rc<List> در هر دو متغیر a و b پس از آن‌که لیست در a را طوری تغییر می‌دهیم که به b اشاره کند، برابر با ۲ خواهد شد. در پایان تابع main، Rust متغیر b را حذف می‌کند، که شمارنده‌ی رفرنس نمونه‌ی Rc<List> مربوط به b را از ۲ به ۱ کاهش می‌دهد. حافظه‌ای که Rc<List> در heap نگه می‌دارد در این لحظه آزاد نخواهد شد، چون شمارنده‌ی رفرنس آن هنوز ۱ است، نه صفر. سپس Rust متغیر a را حذف می‌کند، که شمارنده‌ی رفرنس نمونه‌ی Rc<List> مربوط به a را نیز از ۲ به ۱ کاهش می‌دهد. حافظه‌ی این نمونه نیز نمی‌تواند آزاد شود، زیرا نمونه‌ی دیگر از Rc<List> هنوز به آن اشاره دارد. در نتیجه، حافظه‌ای که به این لیست اختصاص داده شده است، برای همیشه آزاد نخواهد شد.

برای تصویرسازی این چرخه‌ی رفرنس، دیاگرام زیر را در شکل 15-4 ایجاد کرده‌ایم:

مستطیلی با برچسب 'a' که به مستطیلی شامل عدد صحیح 5 اشاره می‌کند. مستطیلی با برچسب 'b' که به مستطیلی شامل عدد صحیح 10 اشاره می‌کند. مستطیل حاوی عدد 5 به مستطیل حاوی عدد 10 اشاره دارد، و مستطیل حاوی عدد 10 دوباره به مستطیل حاوی عدد 5 اشاره دارد، و این یک چرخه ایجاد می‌کند

شکل 15-4: یک چرخه ارجاعی از لیست‌های a و b که به یکدیگر اشاره می‌کنند

اگر آخرین println! را از حالت کامنت خارج کرده و برنامه را اجرا کنید، Rust تلاش خواهد کرد این چرخه را چاپ کند؛ چرخه‌ای که در آن a به b اشاره می‌کند، b به a و دوباره a به b و همین‌طور ادامه پیدا می‌کند تا جایی که پشته (stack) پر شده و stack overflow رخ می‌دهد.

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

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

یک راه‌حل دیگر برای جلوگیری از چرخه‌های ارجاعی، بازسازی ساختار داده‌هایتان است به‌طوری که برخی ارجاعات بیانگر مالکیت باشند و برخی نباشند. به این ترتیب، می‌توانید چرخه‌هایی داشته باشید که شامل برخی روابط مالکیت و برخی روابط غیرمالکیت هستند، و تنها روابط مالکیت تعیین می‌کنند که آیا یک مقدار می‌تواند حذف شود یا خیر. در فهرست 15-25، ما همیشه می‌خواهیم که متغیرهای Cons مالک لیست‌هایشان باشند، بنابراین بازسازی ساختار داده امکان‌پذیر نیست. بیایید به یک مثال با استفاده از گراف‌ها که شامل گره‌های والد و فرزند هستند نگاه کنیم تا ببینیم چه زمانی روابط غیرمالکیت یک راه مناسب برای جلوگیری از چرخه‌های ارجاعی هستند.

جلوگیری از چرخه‌های رفرنس با استفاده از Weak<T>

تا این‌جا نشان دادیم که فراخوانی Rc::clone شمارنده‌ی strong_count یک نمونه‌ی Rc<T> را افزایش می‌دهد، و یک نمونه‌ی Rc<T> تنها زمانی پاک‌سازی می‌شود که مقدار strong_count آن برابر با صفر باشد. همچنین می‌توانید با فراخوانی Rc::downgrade و ارسال یک رفرنس به Rc<T>، یک رفرنس ضعیف (weak reference) به مقدار درون یک نمونه‌ی Rc<T> ایجاد کنید.

رفرنس‌های قوی (strong references) روشی برای به‌اشتراک‌گذاری مالکیت یک نمونه‌ی Rc<T> هستند. در مقابل، رفرنس‌های ضعیف رابطه‌ی مالکیتی ایجاد نمی‌کنند، و شمارش آن‌ها (weak count) هیچ تأثیری در زمان پاک‌سازی یک نمونه‌ی Rc<T> ندارد. آن‌ها باعث ایجاد چرخه‌ی رفرنس نمی‌شوند، زیرا هر چرخه‌ای که شامل برخی weak reference باشد، زمانی شکسته می‌شود که شمارنده‌ی قوی (strong_count) مقادیر درگیر در آن چرخه به صفر برسد.

وقتی Rc::downgrade را فراخوانی می‌کنید، یک اسمارت پوینتر از نوع Weak<T> دریافت می‌کنید. به جای افزایش شمارش strong_count در نمونه Rc<T> به مقدار 1، فراخوانی Rc::downgrade شمارش weak_count را به مقدار 1 افزایش می‌دهد. نوع Rc<T> از weak_count برای پیگیری تعداد ارجاعات Weak<T> موجود استفاده می‌کند، مشابه strong_count. تفاوت این است که شمارش weak_count نیازی به 0 بودن برای پاک‌سازی نمونه Rc<T> ندارد.

از آن‌جا که مقداری که یک Weak<T> به آن اشاره می‌کند ممکن است پیش از این پاک شده باشد، برای انجام هر کاری با آن مقدار ابتدا باید مطمئن شوید که هنوز وجود دارد. برای این کار، متد upgrade را روی یک نمونه‌ی Weak<T> فراخوانی می‌کنید؛ این متد یک Option<Rc<T>> بازمی‌گرداند. اگر مقدار Rc<T> هنوز پاک نشده باشد، نتیجه‌ی Some دریافت خواهید کرد؛ و اگر مقدار Rc<T> قبلاً پاک شده باشد، نتیجه‌ی None خواهد بود. از آن‌جا که upgrade یک Option<Rc<T>> برمی‌گرداند، Rust شما را ملزم می‌کند که هر دو حالت Some و None را مدیریت کنید، و در نتیجه از بروز اشاره‌گر نامعتبر جلوگیری می‌شود.

برای مثال، به جای استفاده از یک لیست که آیتم‌های آن فقط درباره آیتم بعدی اطلاع دارند، ما یک درخت ایجاد خواهیم کرد که آیتم‌های آن درباره آیتم‌های فرزند و والد خود اطلاع دارند.

ایجاد یک ساختار داده‌ی درختی: یک Node با گره‌های فرزند (Child Nodes)

برای شروع، ما یک درخت با گره‌هایی ایجاد خواهیم کرد که درباره گره‌های فرزند خود اطلاع دارند. ما یک ساختار به نام Node ایجاد خواهیم کرد که مقدار i32 خود را نگه می‌دارد و همچنین به گره‌های فرزند خود ارجاع می‌دهد:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

ما می‌خواهیم که یک Node مالک فرزندان خود باشد و همچنین می‌خواهیم که این مالکیت با متغیرها به اشتراک گذاشته شود تا بتوانیم مستقیماً به هر Node در درخت دسترسی داشته باشیم. برای انجام این کار، آیتم‌های Vec<T> را به عنوان مقادیری از نوع Rc<Node> تعریف می‌کنیم. همچنین می‌خواهیم تغییر دهیم که کدام گره‌ها فرزندان یک گره دیگر باشند، بنابراین در children یک RefCell<T> در اطراف Vec<Rc<Node>> قرار می‌دهیم.

در ادامه، از تعریف struct خود استفاده می‌کنیم و یک نمونه از Node با نام leaf ایجاد می‌کنیم که مقدار آن 3 است و هیچ فرزندی ندارد، و یک نمونه‌ی دیگر با نام branch می‌سازیم که مقدار آن 5 است و leaf را به‌عنوان یکی از فرزندان خود دارد؛ همان‌طور که در لیستینگ 15-27 نشان داده شده است.

Filename: src/main.rs
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}
Listing 15-27: ایجاد یک گره leaf بدون فرزند و یک گره branch با leaf به عنوان یکی از فرزندان آن

ما Rc<Node> را در leaf کلون می‌کنیم و آن را در branch ذخیره می‌کنیم، به این معنی که Node در leaf اکنون دو مالک دارد: leaf و branch. ما می‌توانیم از branch به leaf از طریق branch.children برسیم، اما هیچ راهی برای رفتن از leaf به branch وجود ندارد. دلیل این است که leaf هیچ ارجاعی به branch ندارد و نمی‌داند که آن‌ها مرتبط هستند. ما می‌خواهیم که leaf بداند که branch والد آن است. این کار را در مرحله بعد انجام خواهیم داد.

افزودن یک ارجاع از فرزند به والد

برای آگاه کردن گره فرزند از والدش، باید یک فیلد parent به تعریف ساختار Node خود اضافه کنیم. مشکل در تصمیم‌گیری در مورد نوع parent است. می‌دانیم که نمی‌تواند شامل یک Rc<T> باشد، زیرا این امر باعث ایجاد چرخه ارجاعی می‌شود که در آن leaf.parent به branch اشاره می‌کند و branch.children به leaf، که باعث می‌شود مقادیر strong_count آن‌ها هرگز به 0 نرسد.

با در نظر گرفتن روابط از دیدگاهی دیگر، یک گره والد باید مالک فرزندان خود باشد: اگر یک گره والد حذف شود، گره‌های فرزند آن نیز باید حذف شوند. اما، یک فرزند نباید مالک والدش باشد: اگر یک گره فرزند حذف شود، والد باید همچنان وجود داشته باشد. این مورد برای استفاده از ارجاعات ضعیف (weak references) مناسب است!

بنابراین، به جای Rc<T>، نوع parent را از نوع Weak<T> انتخاب می‌کنیم، به طور خاص یک RefCell<Weak<Node>>. اکنون تعریف ساختار Node ما به این شکل است:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

یک گره باید بتواند به گره والد خود اشاره کند، اما مالک آن نباشد. در لیستینگ 15-28، تابع main را به‌روزرسانی می‌کنیم تا از این تعریف جدید استفاده کند و گره leaf بتواند به گره والد خود، یعنی branch، اشاره کند.

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

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
Listing 15-28: یک گره leaf با یک رفرنس ضعیف به گره والدش، branch

ایجاد گره leaf مشابه فهرست 15-27 است با این تفاوت که فیلد parent: leaf ابتدا بدون والد شروع می‌شود، بنابراین یک نمونه جدید و خالی از ارجاع Weak<Node> ایجاد می‌کنیم.

در این مرحله، وقتی سعی می‌کنیم با استفاده از متد upgrade به والد گره leaf دسترسی پیدا کنیم، یک مقدار None دریافت می‌کنیم. این مورد را در خروجی اولین دستور println! مشاهده می‌کنیم:

leaf parent = None

زمانی که گره branch را ایجاد می‌کنیم، این گره در فیلد parent خود دارای یک رفرنس جدید از نوع Weak<Node> خواهد بود، زیرا branch هیچ گره والد ندارد. همچنان leaf یکی از فرزندان branch است. پس از آن‌که نمونه‌ی Node را در branch ساختیم، می‌توانیم گره leaf را طوری تغییر دهیم که یک رفرنس Weak<Node> به والد خود داشته باشد. برای این کار، از متد borrow_mut روی RefCell<Weak<Node>> موجود در فیلد parent در leaf استفاده می‌کنیم، و سپس با استفاده از تابع Rc::downgrade یک رفرنس Weak<Node> به branch می‌سازیم که از Rc<Node> موجود در branch ساخته شده است.

وقتی والد گره leaf را دوباره چاپ می‌کنیم، این بار یک متغیر Some که branch را نگه می‌دارد دریافت می‌کنیم: اکنون leaf می‌تواند به والد خود دسترسی پیدا کند! هنگامی که leaf را چاپ می‌کنیم، همچنین از چرخه‌ای که نهایتاً به سرریز شدن استک مانند فهرست 15-26 منجر می‌شد اجتناب می‌کنیم؛ ارجاعات Weak<Node> به‌صورت (Weak) چاپ می‌شوند:

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

نبود خروجی بی‌نهایت نشان می‌دهد که این کد چرخه ارجاعی ایجاد نکرده است. همچنین می‌توانیم این را با مشاهده مقادیری که از فراخوانی Rc::strong_count و Rc::weak_count دریافت می‌کنیم، تأیید کنیم.

تجسم تغییرات در strong_count و weak_count

بیایید بررسی کنیم که چگونه مقادیر strong_count و weak_count در نمونه‌های Rc<Node> تغییر می‌کنند؛ برای این کار، یک بلاک داخلی جدید ایجاد می‌کنیم و ساخت گره branch را به درون این بلاک منتقل می‌کنیم. با این کار می‌توانیم مشاهده کنیم که چه اتفاقی می‌افتد زمانی که branch ایجاد می‌شود و سپس پس از خارج شدن از حوزه‌ی دید (scope) حذف می‌گردد. این تغییرات در لیستینگ 15-29 نشان داده شده‌اند.

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

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}
Listing 15-29: ایجاد branch در یک دامنه داخلی و بررسی شمارش ارجاعات قوی و ضعیف

پس از ایجاد leaf، مقدار strong_count برای Rc<Node> آن برابر با ۱ و مقدار weak_count برابر با ۰ است. در بلاک داخلی، گره‌ی branch را ایجاد می‌کنیم و آن را به leaf مرتبط می‌سازیم؛ در این مرحله وقتی شمارنده‌ها را چاپ کنیم، Rc<Node> مربوط به branch دارای strong_count برابر با ۱ و weak_count برابر با ۱ خواهد بود (به‌خاطر اینکه leaf.parent به branch با یک Weak<Node> اشاره می‌کند). همچنین وقتی شمارنده‌ها را در leaf چاپ کنیم، مشاهده خواهیم کرد که strong_count آن برابر با ۲ است، زیرا branch اکنون یک کلون از Rc<Node> مربوط به leaf را در branch.children نگه می‌دارد، اما مقدار weak_count آن همچنان ۰ باقی می‌ماند.

وقتی دامنه داخلی به پایان می‌رسد، branch از دامنه خارج می‌شود و شمارش قوی Rc<Node> به 0 کاهش می‌یابد، بنابراین Node آن حذف می‌شود. شمارش ضعیف 1 از leaf.parent تأثیری بر اینکه آیا Node حذف می‌شود ندارد، بنابراین هیچ نشت حافظه‌ای نخواهیم داشت!

اگر بعد از پایان بلاک (scope) تلاش کنیم به والد leaf دسترسی پیدا کنیم، دوباره مقدار None دریافت خواهیم کرد. در انتهای برنامه، مقدار strong_count برای Rc<Node> در leaf برابر با ۱ و مقدار weak_count برابر با ۰ خواهد بود، زیرا متغیر leaf اکنون تنها رفرنس به آن Rc<Node> است.

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

خلاصه

این فصل نحوه استفاده از اسمارت پوینترها برای ارائه تضمین‌ها و مبادلات متفاوت از آنچه که راست به طور پیش‌فرض با ارجاع‌های معمولی ارائه می‌دهد را پوشش داد. نوع Box<T> دارای اندازه مشخصی است و به داده‌های تخصیص‌یافته در heap اشاره می‌کند. نوع Rc<T> تعداد ارجاع‌ها به داده‌ها در heap را پیگیری می‌کند تا داده‌ها بتوانند چندین مالک داشته باشند. نوع RefCell<T> با تغییرپذیری داخلی خود به ما نوعی می‌دهد که می‌توانیم زمانی که به یک نوع غیرقابل‌تغییر نیاز داریم اما باید مقدار درونی آن نوع را تغییر دهیم، استفاده کنیم؛ همچنین قوانین وام‌دهی را در زمان اجرا به جای زمان کامپایل اعمال می‌کند.

همچنین، ویژگی‌های Deref و Drop که بسیاری از قابلیت‌های اسمارت پوینترها را ممکن می‌سازند، مورد بحث قرار گرفتند. ما چرخه‌های ارجاعی که می‌توانند باعث نشت حافظه شوند و نحوه جلوگیری از آن‌ها با استفاده از Weak<T> را بررسی کردیم.

اگر این فصل علاقه شما را برانگیخته و می‌خواهید اسمارت پوینترهای خود را پیاده‌سازی کنید، به “The Rustonomicon” برای اطلاعات مفید بیشتر مراجعه کنید.

در فصل بعدی، درباره همزمانی (concurrency) در راست صحبت خواهیم کرد. حتی با چند اسمارت پوینتر جدید نیز آشنا خواهید شد.