Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 کد کتابخانه را نشان می‌دهد.

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: یک کتابخانه برای پیگیری نزدیکی یک مقدار به یک مقدار حداکثری و هشدار در زمانی که مقدار در سطوح خاصی است

یکی از بخش‌های مهم این کد آن است که 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 اجازه‌ی انجام آن را نمی‌دهد.

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 را به چیزی بیشتر از ۷۵٪ مقدار 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 نشان می‌دهد که این کار چگونه انجام می‌شود.

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> از حوزه‌ی دید (scope) خارج می‌شود، این شمارنده یک واحد کاهش می‌یابد. درست مانند قوانین قرض‌گیری در زمان کامپایل، 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 ایجاد شدند، می‌خواهیم عدد ۱۰ را به مقدار موجود در 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> صحبت خواهیم کرد.