RefCell<T>
و الگوی تغییرپذیری داخلی
تغییرپذیری داخلی یک الگوی طراحی در راست است که به شما اجازه میدهد دادهها را حتی زمانی که
ارجاعهای غیرقابلتغییر به آن دادهها وجود دارد، تغییر دهید؛ معمولاً این عمل توسط قوانین وامدهی
(borrowing rules) ممنوع است. برای تغییر دادهها، این الگو از کد unsafe
درون یک ساختار داده
برای تغییر قوانین معمول راست که کنترل تغییرپذیری و وامدهی را بر عهده دارند، استفاده میکند. کد
unsafe
به کامپایلر نشان میدهد که ما قوانین را به صورت دستی بررسی میکنیم و دیگر به کامپایلر
اعتماد نداریم که این کار را برای ما انجام دهد؛ ما در فصل 20 بیشتر درباره کد unsafe
صحبت خواهیم کرد.
ما میتوانیم از انواعی که از الگوی تغییرپذیری داخلی استفاده میکنند تنها در صورتی استفاده کنیم که
بتوانیم اطمینان حاصل کنیم که قوانین وامدهی در زمان اجرا رعایت خواهند شد، حتی اگر کامپایلر نتواند
این را تضمین کند. کد unsafe
مرتبط سپس در یک API ایمن پیچیده شده و نوع بیرونی همچنان
غیرقابلتغییر باقی میماند.
بیایید این مفهوم را با بررسی نوع RefCell<T>
که از الگوی تغییرپذیری داخلی پیروی میکند،
بیشتر بررسی کنیم.
اجرای قوانین وامدهی در زمان اجرا با RefCell<T>
برخلاف Rc<T>
، نوع RefCell<T>
مالکیت واحد (single ownership) دادههایی که نگه میدارد
را نشان میدهد. پس، چه چیزی RefCell<T>
را از یک نوع مثل Box<T>
متمایز میکند؟ قوانین
وامدهیای که در فصل 4 یاد گرفتید را به یاد آورید:
- در هر زمان معین، شما میتوانید یا (اما نه هر دو) یک ارجاع متغیر یا تعداد زیادی ارجاع غیرقابلتغییر داشته باشید.
- ارجاعها باید همیشه معتبر باشند.
با استفاده از ارجاعها و Box<T>
، ثابتهای قوانین وامدهی در زمان کامپایل اعمال میشوند.
اما با RefCell<T>
، این ثابتها در زمان اجرا اعمال میشوند. با ارجاعها، اگر این قوانین
را بشکنید، یک خطای کامپایل دریافت خواهید کرد. اما با RefCell<T>
، اگر این قوانین را بشکنید،
برنامه شما دچار وحشت (panic) میشود و متوقف میشود.
مزیت بررسی قوانین وامدهی در زمان کامپایل این است که خطاها زودتر در فرایند توسعه شناسایی میشوند، و هیچ تأثیری بر عملکرد زمان اجرا وجود ندارد زیرا تمام تحلیلها پیشاپیش انجام شدهاند. به همین دلایل، بررسی قوانین وامدهی در زمان کامپایل بهترین انتخاب در اکثر موارد است، که به همین دلیل این روش پیشفرض راست است.
مزیت بررسی قوانین وامدهی در زمان اجرا این است که سناریوهایی که ایمن از نظر حافظه هستند اجازه مییابند، در حالی که ممکن است توسط بررسیهای زمان کامپایل مجاز نباشند. تحلیل ایستا (static analysis)، مانند کامپایلر راست، بهطور ذاتی محافظهکارانه است. برخی خصوصیات کد غیرممکن است که با تحلیل کد شناسایی شوند: معروفترین مثال، مشکل توقف (Halting Problem) است که فراتر از محدوده این کتاب است اما موضوع جالبی برای تحقیق میباشد.
زیرا برخی تحلیلها غیرممکن هستند، اگر کامپایلر راست نتواند مطمئن شود که کد با قوانین
مالکیت سازگار است، ممکن است یک برنامه درست را رد کند؛ به این ترتیب، محافظهکارانه عمل
میکند. اگر راست یک برنامه نادرست را بپذیرد، کاربران نمیتوانند به تضمینهایی که راست
میدهد، اعتماد کنند. اما اگر راست یک برنامه درست را رد کند، برنامهنویس ناراحت خواهد شد،
اما هیچ چیز فاجعهباری رخ نخواهد داد. نوع RefCell<T>
زمانی مفید است که مطمئن باشید
کد شما قوانین وامدهی را دنبال میکند اما کامپایلر نمیتواند این را بفهمد و تضمین کند.
مشابه Rc<T>
، RefCell<T>
تنها برای استفاده در سناریوهای تکریسمانی (single-threaded)
است و اگر بخواهید آن را در یک بافت چندریسمانی (multithreaded) استفاده کنید، یک خطای زمان
کامپایل به شما خواهد داد. ما در فصل 16 درباره نحوه دریافت عملکرد RefCell<T>
در یک برنامه
چندریسمانی صحبت خواهیم کرد.
در اینجا مروری بر دلایلی برای انتخاب Box<T>
، Rc<T>
یا RefCell<T>
آمده است:
Rc<T>
امکان چندین مالک برای یک داده را فراهم میکند؛ در حالی کهBox<T>
وRefCell<T>
تنها یک مالک دارند.Box<T>
اجازه میدهد که وامدهیهای غیرقابلتغییر یا قابلتغییر در زمان کامپایل بررسی شوند؛Rc<T>
تنها وامدهیهای غیرقابلتغییر را در زمان کامپایل بررسی میکند؛RefCell<T>
اجازه میدهد که وامدهیهای غیرقابلتغییر یا قابلتغییر در زمان اجرا بررسی شوند.- از آنجا که
RefCell<T>
اجازه میدهد وامدهیهای قابلتغییر در زمان اجرا بررسی شوند، شما میتوانید مقدار درونRefCell<T>
را حتی زمانی که خودRefCell<T>
غیرقابلتغییر است، تغییر دهید.
تغییر مقدار درون یک مقدار غیرقابلتغییر همان الگوی تغییرپذیری داخلی است. بیایید به یک موقعیت که در آن تغییرپذیری داخلی مفید است نگاهی بیندازیم و بررسی کنیم چگونه این امر ممکن است.
تغییرپذیری داخلی: وامدهی قابلتغییر به یک مقدار غیرقابلتغییر
یکی از پیامدهای قوانین وامدهی این است که وقتی شما یک مقدار غیرقابلتغییر دارید، نمیتوانید آن را به صورت قابلتغییر وام دهید. برای مثال، این کد کامپایل نخواهد شد:
fn main() {
let x = 5;
let y = &mut x;
}
اگر سعی کنید این کد را کامپایل کنید، خطای زیر را دریافت خواهید کرد:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
با این حال، موقعیتهایی وجود دارند که در آنها مفید است یک مقدار بتواند خود را در
متدهایش تغییر دهد اما برای کد دیگر غیرقابلتغییر به نظر برسد. کدی که خارج از متدهای
مقدار قرار دارد نمیتواند مقدار را تغییر دهد. استفاده از RefCell<T>
یکی از
راههایی است که میتوانید قابلیت تغییرپذیری داخلی را به دست آورید، اما RefCell<T>
به طور کامل قوانین وامدهی را دور نمیزند: کنترلکننده وامدهی در کامپایلر این
تغییرپذیری داخلی را مجاز میکند و قوانین وامدهی در عوض در زمان اجرا بررسی میشوند.
اگر این قوانین را نقض کنید، به جای خطای کامپایل، یک panic!
دریافت خواهید کرد.
بیایید با یک مثال عملی کار کنیم که در آن از RefCell<T>
برای تغییر مقدار غیرقابلتغییر
استفاده کنیم و ببینیم چرا این کار مفید است.
یک کاربرد برای تغییرپذیری داخلی: Mock Objects
گاهی اوقات در طول تست، یک برنامهنویس از یک نوع به جای نوع دیگری استفاده میکند تا رفتار خاصی را مشاهده کند و اطمینان حاصل کند که به درستی پیادهسازی شده است. این نوع جایگزین تست دابل نامیده میشود. آن را به مانند یک “بدلکار” در فیلمسازی تصور کنید، جایی که یک نفر جایگزین بازیگر میشود تا یک صحنه خاص و دشوار را اجرا کند. تست دابلها به جای انواع دیگر در زمان تست استفاده میشوند. اشیاء Mock نوع خاصی از تست دابلها هستند که ثبت میکنند در طول یک تست چه اتفاقی میافتد تا شما بتوانید اطمینان حاصل کنید که اقدامات صحیح انجام شدهاند.
راست اشیاء را به همان شکلی که زبانهای دیگر دارند، ندارد و قابلیتهای اشیاء Mock را نیز در کتابخانه استاندارد، مانند برخی زبانهای دیگر، ارائه نمیدهد. با این حال، شما میتوانید یک ساختار (struct) ایجاد کنید که همان مقاصد اشیاء Mock را فراهم کند.
در اینجا سناریویی که قصد تست آن را داریم آورده شده است: ما یک کتابخانه ایجاد خواهیم کرد که یک مقدار را نسبت به یک مقدار حداکثری ردیابی میکند و بر اساس نزدیکی مقدار فعلی به مقدار حداکثری پیامهایی ارسال میکند. به عنوان مثال، این کتابخانه میتواند برای پیگیری سهمیه تعداد درخواستهای API که یک کاربر مجاز است انجام دهد، استفاده شود.
کتابخانه ما فقط عملکرد ردیابی نزدیکی یک مقدار به حداکثر و تعیین پیامها در زمانهای
خاص را فراهم خواهد کرد. انتظار میرود برنامههایی که از کتابخانه ما استفاده میکنند
مکانیسم ارسال پیامها را فراهم کنند: برنامه میتواند پیامی را در برنامه قرار دهد، یک
ایمیل ارسال کند، یک پیام متنی ارسال کند، یا چیز دیگری. کتابخانه نیازی به دانستن این
جزئیات ندارد. همه چیزی که نیاز دارد چیزی است که یک ویژگی (trait) به نام
Messenger
که ما ارائه خواهیم کرد را پیادهسازی کند. کد کتابخانه در فهرست
15-20 نشان داده شده است:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
یکی از بخشهای مهم این کد این است که ویژگی Messenger
یک متد به نام send
دارد که یک ارجاع غیرقابلتغییر به self
و متن پیام را میگیرد. این ویژگی رابطی
است که شیء Mock ما باید برای استفاده به همان شیوه که یک شیء واقعی استفاده میشود،
پیادهسازی کند. بخش مهم دیگر این است که ما میخواهیم رفتار متد set_value
را
روی LimitTracker
تست کنیم. ما میتوانیم چیزی را که به عنوان پارامتر به value
میدهیم تغییر دهیم، اما set_value
چیزی برای ما برنمیگرداند که بتوانیم روی آن
ادعا کنیم. ما میخواهیم بتوانیم بگوییم اگر یک LimitTracker
با چیزی که ویژگی
Messenger
را پیادهسازی کرده و مقدار خاصی برای max
ایجاد کنیم، زمانی که
مقادیر مختلفی برای value
ارسال میکنیم، پیامرسان گفته شده است که پیامهای
مناسب را ارسال کند.
ما به یک شیء Mock نیاز داریم که به جای ارسال یک ایمیل یا پیام متنی وقتی که
send
را فراخوانی میکنیم، فقط پیامهایی را که به آن گفته شده است ارسال کند،
پیگیری کند. ما میتوانیم یک نمونه جدید از شیء Mock ایجاد کنیم، یک
LimitTracker
که از شیء Mock استفاده میکند ایجاد کنیم، متد set_value
را
روی LimitTracker
فراخوانی کنیم، و سپس بررسی کنیم که آیا شیء Mock پیامهایی که
انتظار داریم را دارد یا نه. فهرست 15-21 تلاش برای پیادهسازی یک شیء Mock برای
انجام همین کار را نشان میدهد، اما کنترلکننده وامدهی (borrow checker) این اجازه
را نمیدهد:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
MockMessenger
که توسط کنترلکننده وامدهی اجازه داده نمیشوداین کد تست یک ساختار MockMessenger
تعریف میکند که یک فیلد sent_messages
با یک
Vec
از مقادیر String
دارد تا پیامهایی را که به آن گفته شده است ارسال کند،
پیگیری کند. ما همچنین یک تابع مرتبط new
تعریف میکنیم تا ایجاد مقادیر
MockMessenger
جدید که با یک لیست خالی از پیامها شروع میشود، راحت باشد. سپس
ویژگی Messenger
را برای MockMessenger
پیادهسازی میکنیم تا بتوانیم یک
MockMessenger
را به یک LimitTracker
بدهیم. در تعریف متد send
، ما پیام
ارسالشده به عنوان یک پارامتر را میگیریم و آن را در لیست sent_messages
درون MockMessenger
ذخیره میکنیم.
در تست، ما در حال تست این هستیم که وقتی به LimitTracker
گفته میشود مقدار
value
را به چیزی تنظیم کند که بیش از 75 درصد مقدار max
است، چه اتفاقی میافتد.
ابتدا یک MockMessenger
جدید ایجاد میکنیم که با یک لیست خالی از پیامها شروع میشود.
سپس یک LimitTracker
جدید ایجاد میکنیم و یک ارجاع به MockMessenger
جدید و یک
مقدار max
برابر 100 به آن میدهیم. متد set_value
را روی LimitTracker
با
مقدار 80 که بیش از 75 درصد 100 است، فراخوانی میکنیم. سپس ادعا میکنیم که لیست
پیامهایی که MockMessenger
پیگیری میکند اکنون باید یک پیام در آن داشته باشد.
با این حال، یک مشکل با این تست وجود دارد، همانطور که در اینجا نشان داده شده است:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
ما نمیتوانیم MockMessenger
را برای پیگیری پیامها تغییر دهیم، زیرا متد send
یک
ارجاع غیرقابلتغییر به self
میگیرد. همچنین نمیتوانیم پیشنهاد متن خطا را برای استفاده
از &mut self
در هر دو متد impl
و تعریف ویژگی (trait) بپذیریم. ما نمیخواهیم فقط به
خاطر تست، ویژگی Messenger
را تغییر دهیم. در عوض، باید راهی پیدا کنیم که کد تست
ما با طراحی موجود به درستی کار کند.
این یک موقعیت است که در آن تغییرپذیری داخلی میتواند کمک کند! ما فیلد
sent_messages
را درون یک RefCell<T>
ذخیره میکنیم، و سپس متد send
قادر خواهد بود
sent_messages
را برای ذخیره پیامهایی که دیدهایم، تغییر دهد. فهرست 15-22 نشان میدهد
این کار چگونه انجام میشود:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T>
برای تغییر یک مقدار داخلی در حالی که مقدار بیرونی غیرقابلتغییر در نظر گرفته میشودفیلد sent_messages
اکنون از نوع RefCell<Vec<String>>
به جای Vec<String>
است.
در تابع new
، یک نمونه جدید از RefCell<Vec<String>>
را در اطراف وکتور خالی ایجاد
میکنیم.
برای پیادهسازی متد send
، پارامتر اول همچنان یک وامدهی غیرقابلتغییر به self
است، که با تعریف ویژگی مطابقت دارد. ما متد borrow_mut
را روی RefCell<Vec<String>>
در self.sent_messages
فراخوانی میکنیم تا یک ارجاع متغیر به مقدار درون
RefCell<Vec<String>>
، که همان وکتور است، دریافت کنیم. سپس میتوانیم روی ارجاع
متغیر به وکتور، متد push
را فراخوانی کنیم تا پیامهای ارسالشده در طول تست را پیگیری
کنیم.
آخرین تغییری که باید انجام دهیم در ادعا (assertion) است: برای دیدن تعداد آیتمهای
درون وکتور داخلی، ما متد borrow
را روی RefCell<Vec<String>>
فراخوانی میکنیم تا
یک ارجاع غیرقابلتغییر به وکتور دریافت کنیم.
حالا که دیدید چگونه از RefCell<T>
استفاده کنید، بیایید به نحوه کار آن بپردازیم!
پیگیری وامها در زمان اجرا با RefCell<T>
هنگام ایجاد ارجاعهای غیرقابلتغییر و قابلتغییر، ما از سینتکس &
و &mut
استفاده
میکنیم. با RefCell<T>
، از متدهای borrow
و borrow_mut
استفاده میکنیم، که
بخشی از API ایمن متعلق به RefCell<T>
هستند. متد borrow
نوع اسمارت پوینتر
Ref<T>
را برمیگرداند، و borrow_mut
نوع اسمارت پوینتر RefMut<T>
را برمیگرداند.
هر دو نوع، Deref
را پیادهسازی میکنند، بنابراین میتوانیم با آنها مثل ارجاعهای
معمولی رفتار کنیم.
RefCell<T>
تعداد اسمارت پوینترهای Ref<T>
و RefMut<T>
که در حال حاضر فعال هستند
را پیگیری میکند. هر بار که borrow
را فراخوانی میکنیم، RefCell<T>
شمارش تعداد
وامدهیهای غیرقابلتغییر فعال را افزایش میدهد. وقتی یک مقدار Ref<T>
از دامنه
خارج میشود، شمارش وامدهیهای غیرقابلتغییر یک عدد کاهش مییابد. دقیقاً مثل قوانین
وامدهی در زمان کامپایل، RefCell<T>
به ما اجازه میدهد که در هر لحظه تعداد زیادی
وامدهی غیرقابلتغییر یا یک وامدهی قابلتغییر داشته باشیم.
اگر سعی کنیم این قوانین را نقض کنیم، به جای دریافت یک خطای کامپایل مثل ارجاعها،
پیادهسازی RefCell<T>
در زمان اجرا دچار وحشت (panic) خواهد شد. فهرست 15-23
اصلاحی از پیادهسازی متد send
در فهرست 15-22 را نشان میدهد. ما به عمد سعی داریم
دو وامدهی قابلتغییر در یک دامنه ایجاد کنیم تا نشان دهیم RefCell<T>
از انجام
این کار در زمان اجرا جلوگیری میکند.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T>
وحشت خواهد کردما یک متغیر به نام one_borrow
برای اسمارت پوینتر RefMut<T>
که از borrow_mut
بازگردانده شده است، ایجاد میکنیم. سپس یک وامدهی متغیر دیگر به همان روش در
متغیر two_borrow
ایجاد میکنیم. این کار دو ارجاع متغیر در یک دامنه ایجاد میکند،
که مجاز نیست. هنگامی که تستها را برای کتابخانه خود اجرا میکنیم، کد در فهرست
15-23 بدون هیچ خطایی کامپایل میشود، اما تست شکست خواهد خورد:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
توجه داشته باشید که کد با پیام already borrowed: BorrowMutError
دچار وحشت
(panic) شد. این نحوه عملکرد RefCell<T>
برای مدیریت نقض قوانین وامدهی در زمان
اجرا است.
انتخاب اینکه خطاهای وامدهی در زمان اجرا و نه در زمان کامپایل بررسی شوند، همانطور
که در اینجا انجام دادیم، به این معنا است که ممکن است اشتباهات در کد شما در مراحل
بعدی فرآیند توسعه کشف شوند: شاید حتی تا زمانی که کد شما به محیط تولید
(production) استقرار یابد. همچنین، کد شما جریمه عملکردی کوچکی را به دلیل پیگیری
وامها در زمان اجرا به جای زمان کامپایل متحمل خواهد شد. با این حال، استفاده از
RefCell<T>
امکان نوشتن یک شیء Mock را فراهم میکند که میتواند خود را تغییر
دهد تا پیامهایی که مشاهده کرده است را پیگیری کند، در حالی که شما آن را در یک
زمینه که تنها مقادیر غیرقابلتغییر مجاز هستند استفاده میکنید. شما میتوانید
با وجود این مبادلات، از RefCell<T>
برای دریافت عملکرد بیشتری نسبت به
ارجاعهای معمولی استفاده کنید.
داشتن چندین مالک برای دادههای قابلتغییر با ترکیب Rc<T>
و RefCell<T>
یک روش رایج برای استفاده از RefCell<T>
ترکیب آن با Rc<T>
است. به خاطر
بیاورید که Rc<T>
به شما اجازه میدهد چندین مالک برای برخی دادهها داشته
باشید، اما فقط دسترسی غیرقابلتغییر به آن دادهها را میدهد. اگر یک Rc<T>
داشته باشید که یک RefCell<T>
را نگه میدارد، میتوانید یک مقداری داشته باشید
که میتواند چندین مالک داشته باشد و شما بتوانید آن را تغییر دهید!
برای مثال، مثال لیست cons در فهرست 15-18 را به خاطر بیاورید که در آن از Rc<T>
برای اجازه دادن به چندین لیست برای اشتراک مالکیت یک لیست دیگر استفاده کردیم.
چون Rc<T>
تنها مقادیر غیرقابلتغییر را نگه میدارد، نمیتوانیم هیچ یک از مقادیر
در لیست را پس از ایجاد تغییر دهیم. بیایید RefCell<T>
را اضافه کنیم تا توانایی
تغییر مقادیر در لیستها را کسب کنیم. فهرست 15-24 نشان میدهد که با استفاده از
RefCell<T>
در تعریف Cons
، میتوانیم مقدار ذخیرهشده در تمام لیستها را
تغییر دهیم:
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; 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)); *value.borrow_mut() += 10; println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }
Rc<RefCell<i32>>
برای ایجاد یک List
که میتوانیم آن را تغییر دهیمما مقداری که نمونهای از Rc<RefCell<i32>>
است ایجاد میکنیم و آن را در یک
متغیر به نام value
ذخیره میکنیم تا بتوانیم بعداً به طور مستقیم به آن دسترسی
داشته باشیم. سپس یک List
در a
با یک متغیر Cons
که value
را نگه میدارد
ایجاد میکنیم. ما نیاز داریم value
را کلون کنیم تا هر دو a
و value
مالک
مقدار داخلی 5
باشند، به جای انتقال مالکیت از value
به a
یا اینکه a
از
value
وام بگیرد.
ما لیست a
را در یک Rc<T>
میپیچیم تا وقتی که لیستهای b
و c
را ایجاد
میکنیم، هر دو بتوانند به a
ارجاع دهند، که این همان چیزی است که در فهرست 15-18
انجام دادیم.
پس از ایجاد لیستها در a
، b
و c
، میخواهیم 10 به مقدار درون value
اضافه
کنیم. این کار را با فراخوانی borrow_mut
روی value
انجام میدهیم، که از
ویژگی بازارجاع خودکار (automatic dereferencing) که در فصل 5 بحث کردیم (به
بخش «عملگر ->
کجاست؟» مراجعه کنید)
برای بازارجاع Rc<T>
به مقدار داخلی RefCell<T>
استفاده میکند. متد
borrow_mut
یک اسمارت پوینتر RefMut<T>
برمیگرداند، و ما از عملگر بازارجاع
روی آن استفاده میکنیم و مقدار داخلی را تغییر میدهیم.
وقتی a
، b
و c
را چاپ میکنیم، میبینیم که همه آنها مقدار تغییریافته
15 به جای 5 را دارند:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
این تکنیک واقعاً جالب است! با استفاده از RefCell<T>
، ما یک مقدار List
داریم
که به نظر غیرقابلتغییر است. اما میتوانیم از متدهای موجود در RefCell<T>
که
دسترسی به تغییرپذیری داخلی آن را فراهم میکنند استفاده کنیم تا دادههای خود را
هر وقت که نیاز داشتیم تغییر دهیم. بررسیهای زمان اجرا برای قوانین وامدهی ما را
از رقابتهای داده (data races) محافظت میکند، و گاهی اوقات ارزش آن را دارد که
مقداری سرعت را برای این انعطافپذیری در ساختار دادههایمان معامله کنیم. توجه داشته
باشید که RefCell<T>
برای کد چندریسمانی کار نمیکند! نسخه امن برای نخ (thread-safe)
از RefCell<T>
، نوع Mutex<T>
است که در فصل 16 در مورد آن صحبت خواهیم کرد.