Rc<T>, Reference Counted Smart Pointer

বেশিরভাগ ক্ষেত্রে, ownership স্পষ্ট: আপনি জানেন কোন variable একটি নির্দিষ্ট value-র owner। যাইহোক, এমন কিছু ক্ষেত্র আছে যখন একটি single value-র একাধিক owner থাকতে পারে। উদাহরণস্বরূপ, graph ডেটা স্ট্রাকচারে, multiple edge একই node-এর দিকে point করতে পারে এবং সেই node-টি conceptually সেই সমস্ত edge-এর owned যেগুলো সেটির দিকে point করে। একটি node-কে clean up করা উচিত নয় যতক্ষণ না এটির দিকে point করা কোনো edge না থাকে এবং তাই কোনো owner না থাকে।

আপনাকে Rust টাইপ Rc<T> ব্যবহার করে explicitly multiple ownership enable করতে হবে, যেটি reference counting-এর জন্য একটি abbreviation। Rc<T> টাইপ একটি value-তে reference-এর সংখ্যা ট্র্যাক করে যাতে value টি এখনও ব্যবহারে আছে কিনা তা নির্ধারণ করা যায়। যদি একটি value-তে zero reference থাকে, তাহলে কোনো reference invalid না করে value টি clean up করা যেতে পারে।

Rc<T>-কে একটি family room-এর একটি TV-র মতো কল্পনা করুন। যখন একজন ব্যক্তি TV দেখার জন্য প্রবেশ করেন, তখন তারা এটি চালু করেন। অন্যরা ঘরে এসে TV দেখতে পারে। যখন শেষ ব্যক্তি ঘর ছেড়ে চলে যায়, তখন তারা TV বন্ধ করে দেয় কারণ এটি আর ব্যবহার করা হচ্ছে না। যদি অন্য কেউ TV দেখার সময় কেউ TV বন্ধ করে দেয়, তাহলে অবশিষ্ট TV দর্শকদের মধ্যে হট্টগোল হবে!

আমরা Rc<T> টাইপ ব্যবহার করি যখন আমরা আমাদের প্রোগ্রামের multiple part-এর read করার জন্য heap-এ কিছু ডেটা allocate করতে চাই এবং আমরা compile time-এ নির্ধারণ করতে পারি না যে কোন অংশটি ডেটা ব্যবহার করা শেষ করবে। যদি আমরা জানতাম যে কোন অংশটি শেষে শেষ করবে, তাহলে আমরা সেই অংশটিকে ডেটার owner বানাতে পারতাম এবং compile time-এ প্রযোজ্য normal ownership নিয়মগুলো কার্যকর হত।

মনে রাখবেন যে Rc<T> শুধুমাত্র single-threaded scenario-তে ব্যবহারের জন্য। যখন আমরা Chapter 16-এ concurrency নিয়ে আলোচনা করব, তখন আমরা দেখব কিভাবে multithreaded প্রোগ্রামগুলোতে reference counting করতে হয়।

ডেটা Share করার জন্য Rc<T> ব্যবহার করা

আসুন Listing 15-5-এ আমাদের cons list-এর উদাহরণে ফিরে যাই। মনে রাখবেন যে আমরা এটিকে Box<T> ব্যবহার করে define করেছি। এবার, আমরা দুটি list তৈরি করব যারা উভয়ই একটি third list-এর ownership share করবে। ধারণাগতভাবে, এটি Figure 15-3-এর মতো:

দুটি তালিকা যারা একটি তৃতীয় তালিকার ownership share করে

Figure 15-3: দুটি তালিকা, b এবং c, একটি তৃতীয় তালিকা, a-এর ownership share করছে

আমরা list a তৈরি করব যাতে 5 এবং তারপর 10 থাকবে। তারপর আমরা আরও দুটি list তৈরি করব: b যেটি 3 দিয়ে শুরু হবে এবং c যেটি 4 দিয়ে শুরু হবে। B এবং c উভয় list-ই তারপর প্রথম a list-এ continue করবে যেখানে 5 এবং 10 রয়েছে। অন্য কথায়, উভয় list প্রথম list টি share করবে যেখানে 5 এবং 10 রয়েছে।

Box<T> দিয়ে List-এর আমাদের definition ব্যবহার করে এই scenario টি implement করার চেষ্টা করলে কাজ করবে না, যেমনটি Listing 15-17-তে দেখানো হয়েছে:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

যখন আমরা এই কোডটি compile করি, তখন আমরা এই error টি পাই:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Cons variant গুলো তাদের ধারণ করা ডেটার owner, তাই যখন আমরা b list তৈরি করি, তখন a-কে b-তে move করা হয় এবং b, a-এর owner হয়। তারপর, যখন আমরা c তৈরি করার সময় আবার a ব্যবহার করার চেষ্টা করি, তখন আমাদের অনুমতি দেওয়া হয় না কারণ a move করা হয়েছে।

আমরা পরিবর্তে reference ধারণ করার জন্য Cons-এর definition পরিবর্তন করতে পারি, কিন্তু তারপর আমাদের lifetime parameter গুলো specify করতে হবে। Lifetime parameter গুলো specify করে, আমরা specify করব যে list-এর প্রতিটি element পুরো list-এর অন্তত যতদিন বাঁচবে ততদিন বাঁচবে। Listing 15-17-এর element এবং list-গুলোর ক্ষেত্রে এটি প্রযোজ্য, কিন্তু প্রতিটি scenario-তে নয়।

পরিবর্তে, আমরা Listing 15-18-এ দেখানো Box<T>-এর জায়গায় Rc<T> ব্যবহার করার জন্য List-এর আমাদের definition পরিবর্তন করব। প্রতিটি Cons variant-এ এখন একটি value এবং একটি List-এর দিকে point করা একটি Rc<T> থাকবে। যখন আমরা b তৈরি করি, তখন a-এর ownership নেওয়ার পরিবর্তে, আমরা a যে Rc<List> ধারণ করছে সেটিকে clone করব, এইভাবে reference-এর সংখ্যা এক থেকে দুই-এ বৃদ্ধি করব এবং ab-কে সেই Rc<List>-এর ডেটার ownership share করার অনুমতি দেব। C তৈরি করার সময় আমরা a-কে clone করব, reference-এর সংখ্যা দুই থেকে তিনে বাড়িয়ে দেব। প্রতিবার যখন আমরা Rc::clone কল করি, Rc<List>-এর ভেতরের ডেটার reference count বাড়বে এবং ডেটা clean up করা হবে না যতক্ষণ না এটির zero reference থাকে।

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

আমাদের একটি use স্টেটমেন্ট যোগ করতে হবে Rc<T>-কে scope-এ আনার জন্য কারণ এটি prelude-এ নেই। Main-এ, আমরা 5 এবং 10 ধারণকারী list তৈরি করি এবং এটিকে a-তে একটি new Rc<List>-এ store করি। তারপর যখন আমরা b এবং c তৈরি করি, তখন আমরা Rc::clone ফাংশনটি কল করি এবং a-তে Rc<List>-এর একটি reference argument হিসেবে pass করি।

আমরা Rc::clone(&a)-এর পরিবর্তে a.clone() কল করতে পারতাম, কিন্তু Rust-এর convention হল এই ক্ষেত্রে Rc::clone ব্যবহার করা। Rc::clone-এর implementation সমস্ত ডেটার deep copy তৈরি করে না যেমন বেশিরভাগ type-এর clone-এর implementation করে। Rc::clone-এ কল শুধুমাত্র reference count বাড়ায়, যেটিতে বেশি সময় লাগে না। ডেটার Deep copy-তে অনেক সময় লাগতে পারে। Reference counting-এর জন্য Rc::clone ব্যবহার করে, আমরা visually deep-copy ধরনের clone এবং reference count বাড়ায় এমন clone-এর মধ্যে পার্থক্য করতে পারি। কোডে performance-এর সমস্যাগুলো খোঁজার সময়, আমাদের শুধুমাত্র deep-copy clone গুলো বিবেচনা করতে হবে এবং Rc::clone-এ কলগুলো উপেক্ষা করতে পারি।

একটি Rc<T> ক্লোন করা Reference Count বাড়ায়

আসুন Listing 15-18-এ আমাদের working example পরিবর্তন করি যাতে আমরা a-তে Rc<List>-এর reference তৈরি এবং drop করার সাথে সাথে reference count-এর পরিবর্তনগুলো দেখতে পারি।

Listing 15-19-এ, আমরা main পরিবর্তন করব যাতে এটির list c-এর চারপাশে একটি ভেতরের scope থাকে; তারপর আমরা দেখতে পাব কিভাবে reference count পরিবর্তিত হয় যখন c scope-এর বাইরে চলে যায়।

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

প্রোগ্রামের প্রতিটি পয়েন্টে যেখানে reference count পরিবর্তিত হয়, আমরা reference count প্রিন্ট করি, যেটি আমরা Rc::strong_count ফাংশন কল করে পাই। এই ফাংশনটির নাম count-এর পরিবর্তে strong_count রাখা হয়েছে কারণ Rc<T> type-এ একটি weak_count-ও রয়েছে; আমরা "Reference Cycle প্রতিরোধ করা: একটি Rc<T>-কে একটি Weak<T>-তে পরিণত করা"-এ দেখব weak_count কীসের জন্য ব্যবহৃত হয়।

এই কোডটি নিম্নলিখিতগুলো প্রিন্ট করে:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

আমরা দেখতে পাচ্ছি যে a-তে Rc<List>-এর initial reference count হল 1; তারপর প্রতিবার যখন আমরা clone কল করি, count 1 করে বাড়ে। যখন c scope-এর বাইরে চলে যায়, তখন count 1 কমে যায়। Reference count বাড়ানোর জন্য আমাদের Rc::clone কল করতে হবে, সেরকম reference count কমাতে আমাদের কোনো ফাংশন কল করতে হবে না: Drop trait-এর implementation স্বয়ংক্রিয়ভাবে reference count কমিয়ে দেয় যখন একটি Rc<T> value scope-এর বাইরে চলে যায়।

এই উদাহরণে আমরা যা দেখতে পাচ্ছি না তা হল যখন b এবং তারপর a main-এর শেষে scope-এর বাইরে চলে যায়, তখন count 0 হয় এবং Rc<List> সম্পূর্ণরূপে clean up করা হয়। Rc<T> ব্যবহার করা একটি single value-কে multiple owner রাখার অনুমতি দেয় এবং count নিশ্চিত করে যে value-টি valid থাকবে যতক্ষণ পর্যন্ত owner-দের মধ্যে কেউ একজন existing থাকে।

Immutable reference-এর মাধ্যমে, Rc<T> আপনাকে আপনার প্রোগ্রামের multiple part-এর মধ্যে শুধুমাত্র read করার জন্য ডেটা share করার অনুমতি দেয়। যদি Rc<T> আপনাকে multiple mutable reference রাখার অনুমতি দিত, তাহলে আপনি Chapter 4-এ আলোচনা করা borrowing rule-গুলোর মধ্যে একটি লঙ্ঘন করতে পারতেন: একই স্থানে multiple mutable borrow ডেটা রেস এবং অসঙ্গতি সৃষ্টি করতে পারে। কিন্তু ডেটা mutate করতে পারা খুব useful! পরবর্তী section-এ, আমরা interior mutability pattern এবং RefCell<T> type নিয়ে আলোচনা করব যা আপনি এই immutability restriction-এর সাথে কাজ করার জন্য একটি Rc<T>-এর সাথে ব্যবহার করতে পারেন।