چرخههای ارجاعی میتوانند منجر به نشت حافظه شوند
تضمینهای ایمنی حافظه راست ایجاد حافظهای که هرگز پاک نمیشود (که به عنوان نشت حافظه
شناخته میشود) را دشوار میکنند، اما غیرممکن نمیکنند. جلوگیری کامل از نشت حافظه
یکی از تضمینهای راست نیست، به این معنی که نشت حافظه در راست ایمن است. ما میتوانیم
ببینیم که راست اجازه نشت حافظه را میدهد با استفاده از Rc<T>
و RefCell<T>
:
امکان ایجاد ارجاعهایی وجود دارد که آیتمها در آن به یکدیگر در یک چرخه ارجاع میدهند.
این باعث نشت حافظه میشود، زیرا شمارش ارجاع هر آیتم در چرخه هرگز به 0 نمیرسد و
مقادیر هرگز حذف نمیشوند.
ایجاد یک چرخه ارجاعی
بیایید نگاهی بیندازیم که چگونه یک چرخه ارجاعی ممکن است اتفاق بیفتد و چگونه میتوان از آن
جلوگیری کرد، با تعریف enum List
و یک متد tail
در فهرست 15-25 شروع میکنیم:
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() {}
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!
وجود دارند که نشان میدهند شمارش ارجاع
در نقاط مختلف چه مقدار است.
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()); }
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 نشان داده شده است:
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)]), }); }
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
، داشته باشد:
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()); }
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 نشان داده شدهاند:
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), ); }
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) در راست صحبت خواهیم کرد. حتی با چند اسمارت پوینتر جدید نیز آشنا خواهید شد.