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 نشان داده شده است:

Filename: src/lib.rs
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!");
        }
    }
}
Listing 15-20: یک کتابخانه برای پیگیری نزدیکی یک مقدار به یک مقدار حداکثری و هشدار در زمانی که مقدار در سطوح خاصی است

یکی از بخش‌های مهم این کد این است که ویژگی 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) این اجازه را نمی‌دهد:

Filename: src/lib.rs
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);
    }
}
Listing 15-21: تلاش برای پیاده‌سازی یک 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 نشان می‌دهد این کار چگونه انجام می‌شود:

Filename: src/lib.rs
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);
    }
}
Listing 15-22: استفاده از 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> از انجام این کار در زمان اجرا جلوگیری می‌کند.

Filename: src/lib.rs
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);
    }
}
Listing 15-23: ایجاد دو ارجاع متغیر در یک دامنه برای دیدن اینکه 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، می‌توانیم مقدار ذخیره‌شده در تمام لیست‌ها را تغییر دهیم:

Filename: src/main.rs
#[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:?}");
}
Listing 15-24: استفاده از 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 در مورد آن صحبت خواهیم کرد.