RefCell<T>
و الگوی تغییرپذیری داخلی
تغییرپذیری داخلی یک الگوی طراحی در راست است که به شما اجازه میدهد دادهها را حتی زمانی که
ارجاعهای غیرقابلتغییر به آن دادهها وجود دارد، تغییر دهید؛ معمولاً این عمل توسط قوانین وامدهی
(borrowing rules) ممنوع است. برای تغییر دادهها، این الگو از کد unsafe
درون یک ساختار داده
برای تغییر قوانین معمول راست که کنترل تغییرپذیری و وامدهی را بر عهده دارند، استفاده میکند. کد
unsafe
به کامپایلر نشان میدهد که ما قوانین را به صورت دستی بررسی میکنیم و دیگر به کامپایلر
اعتماد نداریم که این کار را برای ما انجام دهد؛ ما در فصل 20 بیشتر درباره کد unsafe
صحبت خواهیم کرد.
ما میتوانیم از انواعی که از الگوی تغییرپذیری داخلی استفاده میکنند تنها در صورتی استفاده کنیم که
بتوانیم اطمینان حاصل کنیم که قوانین وامدهی در زمان اجرا رعایت خواهند شد، حتی اگر کامپایلر نتواند
این را تضمین کند. کد unsafe
مرتبط سپس در یک API ایمن پیچیده شده و نوع بیرونی همچنان
غیرقابلتغییر باقی میماند.
بیایید این مفهوم را با بررسی نوع RefCell<T>
که از الگوی تغییرپذیری داخلی پیروی میکند،
بیشتر بررسی کنیم.
اجرای قوانین وامدهی در زمان اجرا با RefCell<T>
بر خلاف Rc<T>
، نوع RefCell<T>
نشاندهندهی مالکیت یکتا (single ownership) بر دادهای است که در خود نگه میدارد. پس چه چیزی RefCell<T>
را از نوعی مانند Box<T>
متمایز میکند؟ قوانین قرضگیری (borrowing) را که در فصل ۴ یاد گرفتید بهخاطر بیاورید:
- در هر لحظه فقط میتوانید یا یک رفرنس قابلتغییر داشته باشید یا هر تعداد رفرنس تغییرناپذیر (اما نه هر دو همزمان).
- رفرنسها باید همواره معتبر باشند.
با استفاده از ارجاعها و Box<T>
، ثابتهای قوانین وامدهی در زمان کامپایل اعمال میشوند.
اما با RefCell<T>
، این ثابتها در زمان اجرا اعمال میشوند. با ارجاعها، اگر این قوانین
را بشکنید، یک خطای کامپایل دریافت خواهید کرد. اما با RefCell<T>
، اگر این قوانین را بشکنید،
برنامه شما دچار وحشت (panic) میشود و متوقف میشود.
مزیت بررسی قوانین وامدهی در زمان کامپایل این است که خطاها زودتر در فرایند توسعه شناسایی میشوند، و هیچ تأثیری بر عملکرد زمان اجرا وجود ندارد زیرا تمام تحلیلها پیشاپیش انجام شدهاند. به همین دلایل، بررسی قوانین وامدهی در زمان کامپایل بهترین انتخاب در اکثر موارد است، که به همین دلیل این روش پیشفرض راست است.
مزیت بررسی قوانین وامدهی در زمان اجرا این است که سناریوهایی که ایمن از نظر حافظه هستند اجازه مییابند، در حالی که ممکن است توسط بررسیهای زمان کامپایل مجاز نباشند. تحلیل ایستا (static analysis)، مانند کامپایلر راست، بهطور ذاتی محافظهکارانه است. برخی خصوصیات کد غیرممکن است که با تحلیل کد شناسایی شوند: معروفترین مثال، مشکل توقف (Halting Problem) است که فراتر از محدوده این کتاب است اما موضوع جالبی برای تحقیق میباشد.
از آنجا که برخی تحلیلها غیرممکن هستند، اگر کامپایلر Rust نتواند مطمئن شود که کد با قوانین مالکیت سازگار است، ممکن است یک برنامهی درست را رد کند؛ به این ترتیب، کامپایلر محافظهکارانه عمل میکند. اگر Rust یک برنامهی نادرست را بپذیرد، کاربران دیگر نمیتوانند به تضمینهایی که Rust ارائه میدهد اعتماد کنند. اما اگر Rust یک برنامهی درست را رد کند، نهایتاً برنامهنویس دچار زحمت میشود، اما اتفاق فاجعهباری رخ نخواهد داد. نوع 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
گاهی اوقات در زمان تست، یک برنامهنویس نوعی را بهجای نوعی دیگر استفاده میکند تا بتواند رفتار خاصی را مشاهده کرده و بررسی کند که آن رفتار بهدرستی پیادهسازی شده است. این نوع جایگزین را test double مینامند. میتوانید آن را مشابه بدلکار در صنعت فیلمسازی در نظر بگیرید، جایی که فردی بهجای بازیگر اصلی برای اجرای یک صحنهی دشوار وارد عمل میشود. Test doubleها بهعنوان جایگزین نوعهای دیگر هنگام اجرای تستها عمل میکنند. _Mock object_ها نوع خاصی از test doubleها هستند که اتفاقات رخداده در طول تست را ثبت میکنند تا بتوانید بررسی کنید که اقدامات موردنظر بهدرستی انجام شدهاند.
راست اشیاء را به همان شکلی که زبانهای دیگر دارند، ندارد و قابلیتهای اشیاء 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!");
}
}
}
یکی از بخشهای مهم این کد آن است که trait
بهنام Messenger
یک متد بهنام send
دارد که یک رفرنس تغییرناپذیر به self
و متن پیام را میگیرد. این trait
رابطی است که شیء mock ما باید آن را پیادهسازی کند تا بتواند درست مانند یک شیء واقعی مورد استفاده قرار گیرد. بخش مهم دیگر این است که ما میخواهیم رفتار متد set_value
روی LimitTracker
را تست کنیم. ما میتوانیم مقادیری که به پارامتر value
میدهیم را تغییر دهیم، اما set_value
چیزی را باز نمیگرداند که بتوانیم روی آن assertion انجام دهیم. ما میخواهیم بتوانیم بگوییم که اگر یک LimitTracker
با چیزی که trait
Messenger
را پیادهسازی میکند و یک مقدار مشخص برای max
ایجاد کنیم، آنگاه با ارسال اعداد مختلف بهعنوان value
، پیامهای مناسب از طریق messenger
ارسال شوند.
ما به یک شیء 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
را به چیزی بیشتر از ۷۵٪ مقدار max
تنظیم کند، چه اتفاقی میافتد. ابتدا یک MockMessenger
جدید میسازیم که با یک لیست خالی از پیامها شروع میکند. سپس یک LimitTracker
جدید ایجاد میکنیم و یک رفرنس به MockMessenger
جدید و همچنین مقدار max
برابر با 100
به آن میدهیم. متد set_value
را با مقدار 80
روی LimitTracker
فراخوانی میکنیم، که بیش از ۷۵٪ عدد ۱۰۰ است. سپس بررسی میکنیم (assert) که لیست پیامهایی که 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
قرار دهیم، زیرا نمیخواهیم فقط بهخاطر تست، trait
Messenger
را تغییر دهیم. در عوض، باید راهی پیدا کنیم که کد تست ما با طراحی فعلی بهدرستی کار کند.
در چنین وضعیتی، تغییرپذیری درونی (interior mutability) میتواند به کمک ما بیاید! ما فیلد 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>
از حوزهی دید (scope) خارج میشود، این شمارنده یک واحد کاهش مییابد. درست مانند قوانین قرضگیری در زمان کامپایل، 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
ایجاد شدند، میخواهیم عدد ۱۰ را به مقدار موجود در value
اضافه کنیم. این کار را با فراخوانی متد borrow_mut
روی value
انجام میدهیم؛ این متد از قابلیت dereferencing خودکار (که در فصل ۵ در بخش «عملگر ->
کجاست؟» دربارهاش صحبت کردیم) استفاده میکند تا Rc<T>
را به مقدار درونی از نوع RefCell<T>
dereference کند. متد borrow_mut
یک smart pointer از نوع RefMut<T>
برمیگرداند، و ما با استفاده از عملگر *
(dereference) مقدار درونی را تغییر میدهیم.
وقتی 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>
که دسترسی به تغییرپذیری درونی را فراهم میکنند، دادههای خود را در مواقع نیاز تغییر دهیم. بررسیهای زمان اجرا (runtime) برای قوانین قرضگیری از بروز data race جلوگیری میکنند، و گاهی ارزش دارد که اندکی از سرعت را فدای این انعطافپذیری در ساختارهای داده کنیم.
توجه داشته باشید که RefCell<T>
برای کد چندنخی (multithreaded) قابلاستفاده نیست! Mutex<T>
نسخهی ایمن در برابر نخ (thread-safe) از RefCell<T>
است، و ما در فصل ۱۶ دربارهی Mutex<T>
صحبت خواهیم کرد.