পাঠ ১৫.৫

RefCell<T> এবং Interior Mutability pattern

RefCell<T> and the Interior Mutability Pattern

Interior mutability Rust-এর একটা design pattern — immutable reference থাকা সত্ত্বেও সেই data mutate করতে দেয়। সাধারণ borrowing rule এই কাজ disallow করে; pattern-টা type-এর ভেতরে unsafe code দিয়ে rule bend করে — কিন্তু API-এর level-এ runtime check রেখে safety guarantee দেয়।

RefCell<T> — এই pattern-এর সবচেয়ে সাধারণ representative। Box-এর মতো single ownership, কিন্তু borrowing rule compile time-এ না, runtime-এ enforce করে। Rule ভাঙলে compile error না — runtime panic।

Compile-time vs Runtime checking

References আর Box<T> — সব borrowing rule compile time-এ check; ভুল করলে compile error। RefCell<T> — runtime-এ check; rule ভাঙলে program panic করে।

Compile-time checking-এর সুবিধা:

  • Bug development-এর শুরুতে ধরা পড়ে।
  • Runtime cost নেই।

Runtime checking-এর সুবিধা (RefCell):

  • কিছু memory-safe scenario allow হয়, যেগুলো compile-time analysis reject করে।
  • যখন তুমি জানো code rule মানে কিন্তু compiler verify করতে পারছে না।

Box, Rc, RefCell — তফাৎ

  • Ownership: Box single, Rc multiple, RefCell single।
  • Immutable borrow check: Box compile-time, Rc compile-time, RefCell runtime।
  • Mutable borrow check: Box compile-time, Rc allow করে না, RefCell runtime।

Lock-এর মতো — RefCell-এ rule runtime-এ enforced। আর interior mutability-র মূল কথা — RefCell নিজে immutable হলেও তার ভেতরের data mutate করা যায়।

Use case — mock object

Test-এ একটা চমৎকার example। আমরা একটা LimitTracker লাইব্রেরি বানাচ্ছি — একটা Messenger-এ message পাঠায় যখন value quota-এর কাছাকাছি যায়। Trait:

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!");
        }
    }
}

send-এর signature &self — immutable। Test-এ একটা mock messenger দরকার যেটা সব message store করে রাখে — যাতে check করা যায় কোন warning গেছে। কিন্তু &self দিয়ে field mutate করা যায় না।

সমাধান — RefCell:

#[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() {
        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);
    }
}

sent_messages field হয়েছে RefCell<Vec<String>>send-এর ভেতর self.sent_messages.borrow_mut() — runtime-এ mutable borrow নেয়, vector-এ push করে। Test-এ borrow() দিয়ে immutable borrow নিয়ে length check।

Runtime-এ borrow tracking

RefCell-এর দু'টা main method:

  • borrow() — immutable smart pointer Ref<T> দেয়।
  • borrow_mut() — mutable smart pointer RefMut<T> দেয়।

দু'টোই Deref implement করে — regular reference-এর মতো ব্যবহার করা যায়।

RefCell internally count রাখে — কয়টা active Ref এবং RefMut। Rule একই — multiple immutable ঠিক, কিন্তু একটাই mutable এবং সেই সময় immutable থাকতে পারবে না। Rule break-এ panic।

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));
    }
}
runtime panictext
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed

দু'বার borrow_mut() — runtime-এ panic। Compile time-এ এই bug হলে আগেই ধরা যেত; কিন্তু interior mutability pattern-এ এটাই compromise — flexibility-এর বদলে runtime check।

Rc + RefCell — multiple mutable owner

Rc<T> immutable share করে। RefCell<T> single ownership-এ interior mutation দেয়। দু'টো combine করলে — multiple owner যারা সবাই mutate করতে পারবে।

#[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:?}");
}

value shared। b এবং c — দু'জনেই a-কে share করছে, এবং a-এর মধ্যে value-এর reference। তারপর *value.borrow_mut() += 10; — value 5 থেকে 15। Output:

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))

a, b, c সবাই একই 15 দেখছে — কারণ একই RefCell। এই pattern অনেক জটিল data structure-এ দরকার পড়ে।

সতর্কতা

  • RefCell<T> single-threaded only; multi-thread-এ Mutex<T> (চ্যাপ্টার ১৬)।
  • Borrow tracking-এ একটু runtime cost আছে — flexibility-এর জন্য trade-off।
  • যদি code correct হলেও compiler reject করে — তখন এই pattern; কিন্তু যত কম জায়গায় তত ভালো।

এই পাঠ থেকে যা শিখলে

  • Interior mutability pattern — immutable wrapper-এর ভেতরে data mutate; RefCell<T> এর representative।
  • borrow()borrow_mut() — runtime-এ borrow check; rule break-এ panic।
  • Rc<RefCell<T>> — multiple owner + mutable; complex shared data structure-এর জন্য।
  • Single-threaded only; multi-thread-এ Mutex