چرخه‌های ارجاعی می‌توانند منجر به نشت حافظه شوند

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

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

بیایید نگاهی بیندازیم که چگونه یک چرخه ارجاعی ممکن است اتفاق بیفتد و چگونه می‌توان از آن جلوگیری کرد، با تعریف 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> دیگر ایجاد می‌کنیم که مقدار دیگری از 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 برابر با 2 است. در پایان تابع main، راست متغیر b را حذف می‌کند، که شمارش ارجاع نمونه Rc<List> در b را از 2 به 1 کاهش می‌دهد. حافظه‌ای که Rc<List> در heap اشغال کرده است در این نقطه حذف نخواهد شد، زیرا شمارش ارجاع آن برابر با 1 است و نه 0. سپس راست متغیر a را حذف می‌کند، که شمارش ارجاع نمونه Rc<List> در a را نیز از 2 به 1 کاهش می‌دهد. حافظه این نمونه نیز نمی‌تواند حذف شود، زیرا نمونه دیگر Rc<List> همچنان به آن ارجاع می‌دهد. حافظه تخصیص‌یافته به این لیست برای همیشه غیرقابل جمع‌آوری باقی خواهد ماند. برای تجسم این چرخه ارجاع، نموداری در شکل 15-4 ایجاد کرده‌ایم.

چرخه ارجاعی لیست‌ها

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

اگر آخرین دستور println! را از حالت کامنت خارج کنید و برنامه را اجرا کنید، راست سعی خواهد کرد این چرخه را با a که به b و سپس به a اشاره می‌کند و به همین ترتیب ادامه می‌دهد، چاپ کند تا زمانی که استک سرریز شود.

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

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

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

جلوگیری از چرخه‌های ارجاعی: تبدیل یک Rc<T> به یک Weak<T>

تا اینجا، نشان داده‌ایم که فراخوانی Rc::clone شمارش strong_count یک نمونه Rc<T> را افزایش می‌دهد، و یک نمونه Rc<T> تنها زمانی پاک‌سازی می‌شود که شمارش strong_count آن 0 باشد. همچنین می‌توانید با فراخوانی Rc::downgrade و ارسال یک ارجاع به Rc<T>، یک ارجاع ضعیف به مقدار درون یک نمونه Rc<T> ایجاد کنید. ارجاعات قوی به شما اجازه می‌دهند مالکیت یک نمونه Rc<T> را به اشتراک بگذارید. ارجاعات ضعیف یک رابطه مالکیت را بیان نمی‌کنند، و شمارش آن‌ها تأثیری در زمان پاک‌سازی یک نمونه Rc<T> ندارد. آن‌ها باعث ایجاد چرخه ارجاعی نمی‌شوند، زیرا هر چرخه‌ای که شامل برخی ارجاعات ضعیف باشد، وقتی شمارش ارجاع قوی مقادیر درگیر 0 شود، شکسته می‌شود.

وقتی 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> به آن ارجاع می‌دهد ممکن است حذف شده باشد، برای انجام هر کاری با مقداری که یک Weak<T> به آن اشاره می‌کند، باید مطمئن شوید که مقدار هنوز وجود دارد. این کار را با فراخوانی متد upgrade روی یک نمونه Weak<T> انجام دهید، که یک Option<Rc<T>> را برمی‌گرداند. اگر مقدار Rc<T> هنوز حذف نشده باشد، نتیجه Some خواهد بود و اگر مقدار Rc<T> حذف شده باشد، نتیجه None خواهد بود. از آنجا که upgrade یک Option<Rc<T>> را برمی‌گرداند، راست تضمین می‌کند که حالت Some و حالت None مدیریت می‌شوند و هیچ اشاره‌گر (Pointer) نامعتبری وجود نخواهد داشت.

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

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

برای شروع، ما یک درخت با گره‌هایی ایجاد خواهیم کرد که درباره گره‌های فرزند خود اطلاع دارند. ما یک ساختار به نام 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>> قرار می‌دهیم.

سپس، تعریف ساختار خود را استفاده می‌کنیم و یک نمونه 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 را ایجاد می‌کنیم، آن نیز یک ارجاع جدید Weak<Node> در فیلد parent خواهد داشت، زیرا 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 ایجاد و سپس هنگام خارج شدن از دامنه حذف می‌شود. تغییرات در فهرست 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، Rc<Node> آن دارای شمارش قوی 1 و شمارش ضعیف 0 است. در دامنه داخلی، ما branch را ایجاد می‌کنیم و آن را با leaf مرتبط می‌کنیم، در این نقطه وقتی شمارش‌ها را چاپ می‌کنیم، Rc<Node> در branch دارای شمارش قوی 1 و شمارش ضعیف 1 خواهد بود (برای leaf.parent که به branch با یک Weak<Node> اشاره می‌کند). وقتی شمارش‌ها را در leaf چاپ می‌کنیم، می‌بینیم که شمارش قوی آن 2 خواهد بود، زیرا branch اکنون یک کلون از Rc<Node> در leaf که در branch.children ذخیره شده است، دارد، اما همچنان شمارش ضعیف 0 خواهد بود.

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

اگر سعی کنیم پس از پایان دامنه به والد leaf دسترسی پیدا کنیم، دوباره مقدار None دریافت خواهیم کرد. در پایان برنامه، Rc<Node> در leaf دارای شمارش قوی 1 و شمارش ضعیف 0 است، زیرا متغیر 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) در راست صحبت خواهیم کرد. حتی با چند اسمارت پوینتر جدید نیز آشنا خواهید شد.