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> এবং ইন্টেরিয়র মিউটেবিলিটি প্যাটার্ন (Interior Mutability Pattern)

Interior mutability রাস্টের একটি ডিজাইন প্যাটার্ন যা আপনাকে ডেটা পরিবর্তন করার অনুমতি দেয়, এমনকি যখন সেই ডেটার immutable reference থাকে; সাধারণত, borrowing-এর নিয়ম অনুযায়ী এই কাজটি নিষিদ্ধ। ডেটা পরিবর্তন করার জন্য, এই প্যাটার্নটি একটি ডেটা স্ট্রাকচারের ভিতরে unsafe কোড ব্যবহার করে রাস্টের স্বাভাবিক নিয়মাবলী, যা পরিবর্তন এবং borrowing নিয়ন্ত্রণ করে, সেগুলোকে কিছুটা বাঁকিয়ে দেয়। unsafe কোড কম্পাইলারকে নির্দেশ করে যে আমরা নিয়মগুলো ম্যানুয়ালি পরীক্ষা করছি, কম্পাইলারের উপর নির্ভর না করে; আমরা Chapter 20-এ unsafe কোড নিয়ে আরও আলোচনা করব।

আমরা শুধুমাত্র তখনই interior mutability প্যাটার্ন ব্যবহারকারী টাইপগুলো ব্যবহার করতে পারি যখন আমরা নিশ্চিত করতে পারি যে borrowing-এর নিয়মগুলো রানটাইমে অনুসরণ করা হবে, যদিও কম্পাইলার এর গ্যারান্টি দিতে পারে না। ব্যবহৃত unsafe কোডটি তখন একটি নিরাপদ API-এর মধ্যে মোড়ানো থাকে, এবং বাইরের টাইপটি তখনও immutable থাকে।

চলুন, RefCell<T> টাইপটি দেখে এই ধারণাটি অন্বেষেষণ করি, যা interior mutability প্যাটার্ন অনুসরণ করে।

RefCell<T> দিয়ে রানটাইমে Borrowing-এর নিয়ম প্রয়োগ করা

Rc<T>-এর মতো নয়, RefCell<T> টাইপটি তার ধারণ করা ডেটার উপর একক মালিকানা (single ownership) প্রতিনিধিত্ব করে। তাহলে Box<T>-এর মতো টাইপ থেকে RefCell<T> কীভাবে আলাদা? Chapter 4-এ শেখা borrowing-এর নিয়মগুলো মনে করুন:

  • যেকোনো নির্দিষ্ট সময়ে, আপনার কাছে হয় একটি mutable reference অথবা যেকোনো সংখ্যক immutable reference থাকতে পারে (কিন্তু উভয়ই নয়)।
  • Reference সবসময় বৈধ (valid) হতে হবে।

Reference এবং Box<T>-এর ক্ষেত্রে, borrowing-এর নিয়মের এই শর্তগুলো কম্পাইল টাইমে (compile time) প্রয়োগ করা হয়। RefCell<T>-এর ক্ষেত্রে, এই শর্তগুলো রানটাইমে (runtime) প্রয়োগ করা হয়। Reference-এর সাথে, আপনি যদি এই নিয়মগুলো ভঙ্গ করেন, তাহলে আপনি একটি কম্পাইলার এরর পাবেন। RefCell<T>-এর সাথে, আপনি যদি এই নিয়মগুলো ভঙ্গ করেন, আপনার প্রোগ্রামটি প্যানিক (panic) করবে এবং বন্ধ হয়ে যাবে।

কম্পাইল টাইমে borrowing-এর নিয়ম পরীক্ষা করার সুবিধা হলো যে এররগুলো ডেভেলপমেন্ট প্রক্রিয়ার শুরুতেই ধরা পড়ে, এবং রানটাইম পারফরম্যান্সের উপর কোনো প্রভাব পড়ে না কারণ সমস্ত বিশ্লেষণ আগে থেকেই সম্পন্ন হয়। এই কারণগুলোর জন্য, বেশিরভাগ ক্ষেত্রে কম্পাইল টাইমে borrowing-এর নিয়ম পরীক্ষা করাই সেরা পছন্দ, আর একারণেই এটি রাস্টের ডিফল্ট আচরণ।

এর পরিবর্তে রানটাইমে borrowing-এর নিয়ম পরীক্ষা করার সুবিধা হলো যে কিছু মেমরি-সেফ (memory-safe) পরিস্থিতি তখন অনুমোদিত হয়, যা কম্পাইল-টাইম চেক দ্বারা নিষিদ্ধ হতো। স্ট্যাটিক অ্যানালাইসিস (Static analysis), যেমন রাস্ট কম্পাইলার, স্বভাবতই রক্ষণশীল (conservative)। কোডের কিছু বৈশিষ্ট্য কোড বিশ্লেষণ করে সনাক্ত করা অসম্ভব: সবচেয়ে বিখ্যাত উদাহরণ হলো Halting Problem, যা এই বইয়ের আওতার বাইরে কিন্তু গবেষণার জন্য একটি আকর্ষণীয় বিষয়।

যেহেতু কিছু বিশ্লেষণ অসম্ভব, তাই যদি রাস্ট কম্পাইলার নিশ্চিত না হতে পারে যে কোডটি ownership-এর নিয়ম মেনে চলে, তবে এটি একটি সঠিক প্রোগ্রাম প্রত্যাখ্যান করতে পারে; এইভাবে, এটি রক্ষণশীল। যদি রাস্ট একটি ভুল প্রোগ্রাম গ্রহণ করত, ব্যবহারকারীরা রাস্টের দেওয়া গ্যারান্টির উপর বিশ্বাস রাখতে পারত না। তবে, যদি রাস্ট একটি সঠিক প্রোগ্রাম প্রত্যাখ্যান করে, প্রোগ্রামার অসুবিধায় পড়বে, কিন্তু কোনো বিপর্যয় ঘটবে না। RefCell<T> টাইপটি উপযোগী যখন আপনি নিশ্চিত যে আপনার কোড borrowing-এর নিয়ম অনুসরণ করে কিন্তু কম্পাইলার তা বুঝতে এবং গ্যারান্টি দিতে অক্ষম।

Rc<T>-এর মতো, RefCell<T> শুধুমাত্র সিঙ্গেল-থ্রেডেড পরিস্থিতিতে ব্যবহারের জন্য এবং আপনি যদি এটি মাল্টি-থ্রেডেড কনটেক্সটে ব্যবহার করার চেষ্টা করেন তবে এটি আপনাকে একটি কম্পাইল-টাইম এরর দেবে। আমরা Chapter 16-এ একটি মাল্টি-থ্রেডেড প্রোগ্রামে RefCell<T>-এর কার্যকারিতা কীভাবে পাওয়া যায় সে সম্পর্কে কথা বলব।

Box<T>, Rc<T>, বা RefCell<T> বেছে নেওয়ার কারণগুলোর একটি সারসংক্ষেপ নিচে দেওয়া হলো:

  • Rc<T> একই ডেটার একাধিক owner সক্ষম করে; Box<T> এবং RefCell<T>-এর একক owner থাকে।
  • Box<T> কম্পাইল টাইমে চেক করা immutable বা mutable borrow-এর অনুমতি দেয়; Rc<T> শুধুমাত্র কম্পাইল টাইমে চেক করা immutable borrow-এর অনুমতি দেয়; RefCell<T> রানটাইমে চেক করা immutable বা mutable borrow-এর অনুমতি দেয়।
  • যেহেতু RefCell<T> রানটাইমে চেক করা mutable borrow-এর অনুমতি দেয়, তাই আপনি RefCell<T>-এর ভেতরের ভ্যালুটি পরিবর্তন করতে পারেন এমনকি যখন RefCell<T>-টি immutable থাকে।

একটি immutable ভ্যালুর ভেতরের ভ্যালু পরিবর্তন করাই হলো interior mutability প্যাটার্ন। চলুন এমন একটি পরিস্থিতি দেখি যেখানে interior mutability উপযোগী এবং এটি কীভাবে সম্ভব তা পরীক্ষা করি।

ইন্টেরিয়র মিউটেবিলিটি: একটি Immutable ভ্যালুর জন্য Mutable Borrow

Borrowing-এর নিয়মের একটি ফলাফল হলো যখন আপনার কাছে একটি immutable ভ্যালু থাকে, আপনি এটিকে mutable-ভাবে borrow করতে পারবেন না। উদাহরণস্বরূপ, এই কোডটি কম্পাইল হবে না:

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

তবে, এমন পরিস্থিতি আছে যেখানে একটি ভ্যালুর জন্য তার মেথডগুলোতে নিজেকে পরিবর্তন করা উপযোগী হবে কিন্তু অন্য কোডের কাছে এটি immutable বলে মনে হবে। ভ্যালুর মেথডগুলোর বাইরের কোড ভ্যালুটি পরিবর্তন করতে পারবে না। RefCell<T> ব্যবহার করা interior mutability-র ক্ষমতা পাওয়ার একটি উপায়, কিন্তু RefCell<T> borrowing-এর নিয়মগুলো পুরোপুরি এড়িয়ে যায় না: কম্পাইলারের borrow checker এই interior mutability-কে অনুমতি দেয়, এবং borrowing-এর নিয়মগুলো রানটাইমে পরীক্ষা করা হয়। যদি আপনি নিয়ম লঙ্ঘন করেন, তাহলে আপনি কম্পাইলার এররের পরিবর্তে একটি panic! পাবেন।

চলুন একটি বাস্তব উদাহরণ দেখি যেখানে আমরা RefCell<T> ব্যবহার করে একটি immutable ভ্যালু পরিবর্তন করতে পারি এবং দেখি কেন এটি উপযোগী।

ইন্টেরিয়র মিউটেবিলিটির একটি ব্যবহার: মক অবজেক্ট (Mock Objects)

কখনও কখনও টেস্টিংয়ের সময় একজন প্রোগ্রামার একটি নির্দিষ্ট আচরণ পর্যবেক্ষণ করতে এবং এটি সঠিকভাবে ইমপ্লিমেন্ট করা হয়েছে কিনা তা নিশ্চিত করতে অন্য একটি টাইপের জায়গায় একটি টাইপ ব্যবহার করেন। এই placeholder টাইপটিকে বলা হয় test double। এটিকে ফিল্মমেকিং-এর স্টান্ট ডাবলের মতো ভাবুন, যেখানে একজন ব্যক্তি একটি বিশেষভাবে কঠিন দৃশ্যের জন্য একজন অভিনেতার পরিবর্তে কাজ করে। আমরা যখন টেস্ট চালাই তখন টেস্ট ডাবলগুলো অন্য টাইপের জন্য দাঁড়িয়ে থাকে। Mock objects হলো বিশেষ ধরনের টেস্ট ডাবল যা একটি টেস্টের সময় কী ঘটে তা রেকর্ড করে যাতে আপনি assert করতে পারেন যে সঠিক কাজগুলো হয়েছে।

অন্যান্য ভাষায় যেমন অবজেক্ট আছে, রাস্ট-এ সেই অর্থে অবজেক্ট নেই, এবং রাস্টের স্ট্যান্ডার্ড লাইব্রেরিতে অন্য কিছু ভাষার মতো মক অবজেক্ট কার্যকারিতা বিল্ট-ইন নেই। তবে, আপনি অবশ্যই একটি struct তৈরি করতে পারেন যা একটি মক অবজেক্টের মতো একই উদ্দেশ্যে কাজ করবে।

এখানে আমরা যে পরিস্থিতিটি পরীক্ষা করব: আমরা একটি লাইব্রেরি তৈরি করব যা একটি সর্বোচ্চ মানের (maximum value) বিপরীতে একটি মান ট্র্যাক করে এবং বর্তমান মানটি সর্বোচ্চ মানের কতটা কাছাকাছি তার উপর ভিত্তি করে বার্তা পাঠায়। এই লাইব্রেরিটি একজন ব্যবহারকারীর API কল করার কোটা ট্র্যাক করতে ব্যবহার করা যেতে পারে, উদাহরণস্বরূপ।

আমাদের লাইব্রেরি শুধুমাত্র একটি মান সর্বোচ্চ মানের কতটা কাছাকাছি তা ট্র্যাক করার কার্যকারিতা এবং কোন সময়ে কী বার্তা হওয়া উচিত তা সরবরাহ করবে। আমাদের লাইব্রেরি ব্যবহারকারী অ্যাপ্লিকেশনগুলো থেকে বার্তা পাঠানোর ব্যবস্থা সরবরাহ করার আশা করা হবে: অ্যাপ্লিকেশনটি অ্যাপ্লিকেশনে একটি বার্তা রাখতে পারে, একটি ইমেল পাঠাতে পারে, একটি টেক্সট বার্তা পাঠাতে পারে, বা অন্য কিছু করতে পারে। লাইব্রেরিকে সেই বিস্তারিত জানার প্রয়োজন নেই। এটির যা প্রয়োজন তা হলো এমন কিছু যা আমরা Messenger নামে একটি ট্রেইট সরবরাহ করব তা ইমপ্লিমেন্ট করে। Listing 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-এর একটি immutable reference এবং বার্তার টেক্সট নেয়। এই ট্রেইটটি হলো সেই ইন্টারফেস যা আমাদের মক অবজেক্টকে ইমপ্লিমেন্ট করতে হবে যাতে মকটি একটি আসল অবজেক্টের মতো একইভাবে ব্যবহার করা যায়। অন্য গুরুত্বপূর্ণ অংশ হলো আমরা LimitTracker-এর set_value মেথডের আচরণ পরীক্ষা করতে চাই। আমরা value প্যারামিটারের জন্য যা পাস করি তা পরিবর্তন করতে পারি, কিন্তু set_value আমাদের assert করার জন্য কিছু রিটার্ন করে না। আমরা বলতে চাই যে যদি আমরা Messenger ট্রেইট ইমপ্লিমেন্ট করে এমন কিছু এবং max-এর জন্য একটি নির্দিষ্ট মান দিয়ে একটি LimitTracker তৈরি করি, যখন আমরা value-এর জন্য বিভিন্ন সংখ্যা পাস করি তখন মেসেঞ্জারকে উপযুক্ত বার্তা পাঠাতে বলা হয়।

আমাদের একটি মক অবজেক্ট দরকার যা, send কল করার সময় ইমেল বা টেক্সট বার্তা পাঠানোর পরিবর্তে, শুধুমাত্র তাকে যে বার্তাগুলো পাঠাতে বলা হয়েছে সেগুলো ট্র্যাক করবে। আমরা মক অবজেক্টের একটি নতুন ইনস্ট্যান্স তৈরি করতে পারি, মক অবজেক্ট ব্যবহার করে এমন একটি LimitTracker তৈরি করতে পারি, LimitTracker-এর set_value মেথড কল করতে পারি, এবং তারপর পরীক্ষা করতে পারি যে মক অবজেক্টে আমাদের প্রত্যাশিত বার্তাগুলো আছে কিনা। Listing 15-21 একটি মক অবজেক্ট ইমপ্লিমেন্ট করার একটি প্রচেষ্টা দেখায়, কিন্তু 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 struct সংজ্ঞায়িত করে যার sent_messages নামে একটি ফিল্ড আছে যেখানে Vec<String> মানের একটি ভেক্টর রয়েছে যা তাকে পাঠানো বার্তাগুলো ট্র্যাক করার জন্য। আমরা একটি new নামের associated function-ও সংজ্ঞায়িত করি যাতে খালি বার্তা তালিকা দিয়ে নতুন MockMessenger মান তৈরি করা সুবিধাজনক হয়। তারপর আমরা MockMessenger-এর জন্য Messenger ট্রেইট ইমপ্লিমেন্ট করি যাতে আমরা একটি MockMessenger-কে একটি LimitTracker-কে দিতে পারি। send মেথডের সংজ্ঞায়, আমরা প্যারামিটার হিসাবে পাস করা বার্তাটি নিই এবং এটিকে MockMessenger-এর sent_messages তালিকায় সংরক্ষণ করি।

টেস্টে, আমরা পরীক্ষা করছি যে LimitTracker-কে value এমন কিছুতে সেট করতে বলা হলে কী হয় যা max মানের ৭৫ শতাংশের বেশি। প্রথমে আমরা একটি নতুন MockMessenger তৈরি করি, যা একটি খালি বার্তা তালিকা দিয়ে শুরু হবে। তারপর আমরা একটি নতুন LimitTracker তৈরি করি এবং এটিকে নতুন MockMessenger-এর একটি রেফারেন্স এবং 100-এর একটি max মান দিই। আমরা LimitTracker-এর set_value মেথডটি 80 মান দিয়ে কল করি, যা ১০০-এর ৭৫ শতাংশের বেশি। তারপর আমরা 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-এর একটি immutable reference নেয়। আমরা এরর টেক্সট থেকে &mut self ব্যবহার করার পরামর্শটিও নিতে পারি না। আমরা শুধুমাত্র টেস্টিংয়ের জন্য Messenger ট্রেইট পরিবর্তন করতে চাই না। পরিবর্তে, আমাদের বিদ্যমান ডিজাইনের সাথে আমাদের টেস্ট কোড সঠিকভাবে কাজ করার একটি উপায় খুঁজে বের করতে হবে।

এটি এমন একটি পরিস্থিতি যেখানে ইন্টেরিয়র মিউটেবিলিটি সাহায্য করতে পারে! আমরা sent_messages-কে একটি RefCell<T>-এর মধ্যে সংরক্ষণ করব, এবং তারপর send মেথড sent_messages পরিবর্তন করতে সক্ষম হবে যাতে আমরা যে বার্তাগুলো দেখেছি তা সংরক্ষণ করতে পারে। Listing 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);
    }
}

sent_messages ফিল্ডটি এখন Vec<String>-এর পরিবর্তে RefCell<Vec<String>> টাইপের। new ফাংশনে, আমরা খালি ভেক্টরের চারপাশে একটি নতুন RefCell<Vec<String>> ইনস্ট্যান্স তৈরি করি।

send মেথডের ইমপ্লিমেন্টেশনের জন্য, প্রথম প্যারামিটারটি এখনও self-এর একটি immutable borrow, যা ট্রেইট সংজ্ঞার সাথে মেলে। আমরা self.sent_messages-এর RefCell<Vec<String>>-এর উপর borrow_mut কল করি যাতে RefCell<Vec<String>>-এর ভেতরের ভ্যালু, যা হলো ভেক্টর, তার একটি mutable reference পেতে পারি। তারপর আমরা ভেক্টরের mutable reference-এর উপর push কল করতে পারি যাতে টেস্টের সময় পাঠানো বার্তাগুলো ট্র্যাক রাখা যায়।

শেষ যে পরিবর্তনটি আমাদের করতে হবে তা হলো assertion-এ: ভেতরের ভেক্টরে কতগুলো আইটেম আছে তা দেখতে, আমরা RefCell<Vec<String>>-এর উপর borrow কল করি যাতে ভেক্টরের একটি immutable reference পেতে পারি।

এখন যেহেতু আপনি RefCell<T> কীভাবে ব্যবহার করতে হয় তা দেখেছেন, আসুন আমরা দেখি এটি কীভাবে কাজ করে!

RefCell<T> এর মাধ্যমে রানটাইমে Borrow ট্র্যাক রাখা

Immutable এবং mutable reference তৈরি করার সময়, আমরা যথাক্রমে & এবং &mut সিনট্যাক্স ব্যবহার করি। RefCell<T>-এর সাথে, আমরা borrow এবং borrow_mut মেথড ব্যবহার করি, যা RefCell<T>-এর নিরাপদ API-এর অংশ। borrow মেথডটি স্মার্ট পয়েন্টার টাইপ Ref<T> রিটার্ন করে, এবং borrow_mut স্মার্ট পয়েন্টার টাইপ RefMut<T> রিটার্ন করে। উভয় টাইপই Deref ইমপ্লিমেন্ট করে, তাই আমরা তাদের সাধারণ reference-এর মতো ব্যবহার করতে পারি।

RefCell<T> ট্র্যাক রাখে যে বর্তমানে কতগুলো Ref<T> এবং RefMut<T> স্মার্ট পয়েন্টার সক্রিয় আছে। প্রতিবার যখন আমরা borrow কল করি, RefCell<T> তার সক্রিয় immutable borrow-এর সংখ্যা বাড়িয়ে দেয়। যখন একটি Ref<T> ভ্যালু স্কোপের বাইরে চলে যায়, immutable borrow-এর সংখ্যা ১ কমে যায়। ঠিক কম্পাইল-টাইম borrowing নিয়মের মতোই, RefCell<T> আমাদের যেকোনো সময়ে অনেকগুলো immutable borrow অথবা একটি mutable borrow রাখার অনুমতি দেয়।

যদি আমরা এই নিয়মগুলো লঙ্ঘন করার চেষ্টা করি, তাহলে reference-এর ক্ষেত্রে যেমন কম্পাইলার এরর পেতাম, তার পরিবর্তে RefCell<T>-এর ইমপ্লিমেন্টেশন রানটাইমে প্যানিক করবে। Listing 15-23 Listing 15-22-এর send-এর ইমপ্লিমেন্টেশনের একটি পরিবর্তন দেখায়। আমরা ইচ্ছাকৃতভাবে একই স্কোপের জন্য দুটি সক্রিয় mutable borrow তৈরি করার চেষ্টা করছি যাতে দেখানো যায় যে 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);
    }
}

আমরা borrow_mut থেকে রিটার্ন করা RefMut<T> স্মার্ট পয়েন্টারের জন্য one_borrow নামে একটি ভ্যারিয়েবল তৈরি করি। তারপর আমরা two_borrow ভ্যারিয়েবলে একইভাবে আরেকটি mutable borrow তৈরি করি। এটি একই স্কোপে দুটি mutable reference তৈরি করে, যা অনুমোদিত নয়। যখন আমরা আমাদের লাইব্রেরির জন্য টেস্ট চালাই, Listing 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 বার্তা দিয়ে প্যানিক করেছে। এভাবেই RefCell<T> রানটাইমে borrowing নিয়মের লঙ্ঘন সামাল দেয়।

কম্পাইল টাইমের পরিবর্তে রানটাইমে borrowing এরর ধরার সিদ্ধান্ত নেওয়ার মানে হলো, যেমনটি আমরা এখানে করেছি, আপনি সম্ভবত ডেভেলপমেন্ট প্রক্রিয়ার পরে আপনার কোডের ভুল খুঁজে পাবেন: সম্ভবত আপনার কোড প্রোডাকশনে স্থাপন না হওয়া পর্যন্ত নয়। এছাড়াও, রানটাইমে borrow ট্র্যাক রাখার ফলে আপনার কোডে একটি ছোট রানটাইম পারফরম্যান্স পেনাল্টি হবে। তবে, RefCell<T> ব্যবহার করে এমন একটি মক অবজেক্ট লেখা সম্ভব যা নিজেকে পরিবর্তন করে তার দেখা বার্তাগুলোর ট্র্যাক রাখতে পারে যখন আপনি এটি এমন একটি কনটেক্সটে ব্যবহার করছেন যেখানে শুধুমাত্র immutable ভ্যালু অনুমোদিত। আপনি নিয়মিত reference-এর চেয়ে বেশি কার্যকারিতা পেতে RefCell<T> ব্যবহার করতে পারেন, এর ট্রেড-অফ থাকা সত্ত্বেও।

Rc<T> এবং RefCell<T> একত্রিত করে একাধিক মালিকানাধীন Mutable ডেটার অনুমতি দেওয়া

RefCell<T> ব্যবহার করার একটি সাধারণ উপায় হলো Rc<T>-এর সাথে সংমিশ্রণ। মনে করুন Rc<T> আপনাকে কিছু ডেটার একাধিক owner रखने দেয়, কিন্তু এটি শুধুমাত্র সেই ডেটার immutable অ্যাক্সেস দেয়। যদি আপনার কাছে একটি Rc<T> থাকে যা একটি RefCell<T> ধারণ করে, আপনি এমন একটি ভ্যালু পেতে পারেন যার একাধিক owner থাকতে পারে এবং যা আপনি পরিবর্তন করতে পারেন!

উদাহরণস্বরূপ, Listing 15-18-এর cons list উদাহরণটি মনে করুন যেখানে আমরা Rc<T> ব্যবহার করে একাধিক লিস্টকে অন্য একটি লিস্টের মালিকানা শেয়ার করার অনুমতি দিয়েছিলাম। যেহেতু Rc<T> শুধুমাত্র immutable ভ্যালু ধারণ করে, তাই আমরা একবার লিস্ট তৈরি করার পরে লিস্টের কোনো ভ্যালু পরিবর্তন করতে পারি না। চলুন লিস্টের ভ্যালুগুলো পরিবর্তন করার ক্ষমতার জন্য RefCell<T> যোগ করি। Listing 15-24 দেখাচ্ছে যে Cons সংজ্ঞায় একটি RefCell<T> ব্যবহার করে, আমরা সমস্ত লিস্টে সংরক্ষিত ভ্যালু পরিবর্তন করতে পারি।

#[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>>-এর একটি ইনস্ট্যান্স তৈরি করে value নামের একটি ভেরিয়েবলে সংরক্ষণ করি যাতে আমরা পরে সরাসরি এটি অ্যাক্সেস করতে পারি। তারপর আমরা a-তে একটি List তৈরি করি যার Cons ভ্যারিয়েন্ট value ধারণ করে। আমাদের value ক্লোন করতে হবে যাতে a এবং value উভয়েই ভেতরের 5 ভ্যালুর মালিকানা পায়, value থেকে a-তে মালিকানা হস্তান্তর না করে বা a-কে value থেকে borrow করতে না হয়।

আমরা a লিস্টটিকে একটি Rc<T>-তে মুড়িয়ে দিই যাতে যখন আমরা b এবং c লিস্ট তৈরি করি, তারা উভয়েই a-কে নির্দেশ করতে পারে, যা আমরা Listing 15-18-এ করেছিলাম।

a, b, এবং c-তে লিস্ট তৈরি করার পরে, আমরা value-এর ভ্যালুতে 10 যোগ করতে চাই। আমরা value-এর উপর borrow_mut কল করে এটি করি, যা Chapter 5-এর ["Where’s the -> Operator?"][wheres-the---operator]-এ আলোচনা করা স্বয়ংক্রিয় dereferencing ফিচার ব্যবহার করে Rc<T>-কে ভেতরের RefCell<T> ভ্যালুতে dereference করে। borrow_mut মেথডটি একটি RefMut<T> স্মার্ট পয়েন্টার রিটার্ন করে, এবং আমরা এর উপর dereference অপারেটর ব্যবহার করে ভেতরের ভ্যালু পরিবর্তন করি।

যখন আমরা a, b, এবং c প্রিন্ট করি, আমরা দেখতে পাই যে তাদের সকলেরই 5-এর পরিবর্তে পরিবর্তিত মান 15 রয়েছে:

$ 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> ব্যবহার করে, আমাদের কাছে একটি বাহ্যিকভাবে immutable List ভ্যালু আছে। কিন্তু আমরা RefCell<T>-এর মেথডগুলো ব্যবহার করতে পারি যা তার ইন্টেরিয়র মিউটেবিলিটিতে অ্যাক্সেস দেয় যাতে প্রয়োজনে আমরা আমাদের ডেটা পরিবর্তন করতে পারি। রানটাইমে borrowing নিয়মের চেক আমাদের ডেটা রেস থেকে রক্ষা করে, এবং আমাদের ডেটা স্ট্রাকচারে এই নমনীয়তার জন্য কখনও কখনও কিছুটা গতি বিসর্জন দেওয়া সার্থক। মনে রাখবেন RefCell<T> মাল্টি-থ্রেডেড কোডের জন্য কাজ করে না! Mutex<T> হলো RefCell<T>-এর থ্রেড-সেফ সংস্করণ, এবং আমরা Chapter 16-এ Mutex<T> নিয়ে আলোচনা করব।