Rc<T>
, রেফারেন্স কাউন্টেড স্মার্ট পয়েন্টার (Reference Counted Smart Pointer)
বেশিরভাগ ক্ষেত্রে, মালিকানা (ownership) স্পষ্ট থাকে: আপনি ঠিক জানেন কোন ভ্যারিয়েবল কোন ভ্যালুর মালিক। তবে, এমন কিছু ক্ষেত্র আছে যেখানে একটিমাত্র ভ্যালুর একাধিক মালিক থাকতে পারে। উদাহরণস্বরূপ, গ্রাফ ডেটা স্ট্রাকচারে, একাধিক এজ (edge) একই নোডকে (node) নির্দেশ করতে পারে, এবং সেই নোডটি ধারণাগতভাবে সেই সমস্ত এজের মালিকানাধীন থাকে যারা তাকে নির্দেশ করে। একটি নোড ততক্ষণ পর্যন্ত পরিষ্কার করা উচিত নয় যতক্ষণ না পর্যন্ত কোনো এজ তাকে নির্দেশ করছে এবং তার কোনো মালিক নেই।
আপনাকে রাস্টের Rc<T>
টাইপ ব্যবহার করে স্পষ্টভাবে একাধিক মালিকানা সক্রিয় করতে হবে, যা reference counting-এর সংক্ষিপ্ত রূপ। Rc<T>
টাইপটি একটি ভ্যালুর রেফারেন্সের সংখ্যা ট্র্যাক করে তা নির্ধারণ করার জন্য যে ভ্যালুটি এখনও ব্যবহৃত হচ্ছে কিনা। যদি একটি ভ্যালুর রেফারেন্স সংখ্যা শূন্য হয়, তবে কোনো রেফারেন্স অবৈধ না করেই ভ্যালুটি পরিষ্কার করা যেতে পারে।
Rc<T>
-কে একটি বসার ঘরের টিভির মতো কল্পনা করুন। যখন একজন ব্যক্তি টিভি দেখতে প্রবেশ করে, তখন সে টিভি চালু করে। অন্যরা ঘরে এসে টিভি দেখতে পারে। যখন শেষ ব্যক্তি ঘর থেকে বেরিয়ে যায়, তখন সে টিভি বন্ধ করে দেয় কারণ এটি আর ব্যবহৃত হচ্ছে না। যদি অন্য কেউ টিভি দেখার সময় টিভি বন্ধ করে দেয়, তবে বাকি টিভি দর্শকদের মধ্যে হৈচৈ পড়ে যাবে!
আমরা Rc<T>
টাইপটি ব্যবহার করি যখন আমরা আমাদের প্রোগ্রামের একাধিক অংশের জন্য হিপে কিছু ডেটা বরাদ্দ করতে চাই যা শুধু পড়া হবে এবং আমরা কম্পাইল টাইমে নির্ধারণ করতে পারি না কোন অংশটি ডেটা ব্যবহার করা শেষ করবে। যদি আমরা জানতাম কোন অংশটি শেষে শেষ করবে, আমরা কেবল সেই অংশটিকে ডেটার মালিক করতে পারতাম, এবং কম্পাইল টাইমে প্রয়োগ করা সাধারণ মালিকানার নিয়ম কার্যকর হত।
মনে রাখবেন Rc<T>
শুধুমাত্র সিঙ্গেল-থ্রেডেড (single-threaded) পরিস্থিতিতে ব্যবহারের জন্য। যখন আমরা Chapter 16-এ কনকারেন্সি (concurrency) নিয়ে আলোচনা করব, তখন আমরা মাল্টি-থ্রেডেড (multithreaded) প্রোগ্রামে কীভাবে রেফারেন্স কাউন্টিং করতে হয় তা দেখব।
Rc<T>
ব্যবহার করে ডেটা শেয়ার করা
চলুন Listing 15-5-এর আমাদের cons list-এর উদাহরণে ফিরে যাই। মনে করে দেখুন, আমরা এটি Box<T>
ব্যবহার করে সংজ্ঞায়িত করেছিলাম। এবার, আমরা দুটি লিস্ট তৈরি করব যারা উভয়েই তৃতীয় একটি লিস্টের মালিকানা শেয়ার করবে। ধারণাগতভাবে, এটি Figure 15-3-এর মতো দেখায়।
Figure 15-3: দুটি লিস্ট, b
এবং c
, তৃতীয় একটি লিস্ট a
-এর মালিকানা শেয়ার করছে
আমরা a
লিস্ট তৈরি করব যা 5
এবং তারপর 10
ধারণ করবে। তারপর আমরা আরও দুটি লিস্ট তৈরি করব: b
যা 3
দিয়ে শুরু হবে এবং c
যা 4
দিয়ে শুরু হবে। b
এবং c
উভয় লিস্টই তারপর প্রথম a
লিস্টে চলবে যা 5
এবং 10
ধারণ করে। অন্য কথায়, উভয় লিস্টই 5
এবং 10
ধারণকারী প্রথম লিস্টটি শেয়ার করবে।
Box<T>
দিয়ে আমাদের List
-এর সংজ্ঞা ব্যবহার করে এই পরিস্থিতিটি ইমপ্লিমেন্ট করার চেষ্টা করলে কাজ করবে না, যেমনটি 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));
}
যখন আমরা এই কোডটি কম্পাইল করি, তখন আমরা এই এররটি পাই:
$ 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
ভ্যারিয়েন্টগুলো তাদের ধারণ করা ডেটার মালিক, তাই যখন আমরা b
লিস্ট তৈরি করি, a
কে b
-তে মুভ (move) করা হয় এবং b
a
-এর মালিক হয়ে যায়। তারপর, যখন আমরা c
তৈরি করার সময় আবার a
ব্যবহার করার চেষ্টা করি, তখন আমাদের অনুমতি দেওয়া হয় না কারণ a
মুভ হয়ে গেছে।
আমরা Cons
-এর সংজ্ঞা পরিবর্তন করে রেফারেন্স ধারণ করতে পারতাম, কিন্তু তাহলে আমাদের লাইফটাইম প্যারামিটার (lifetime parameters) নির্দিষ্ট করতে হতো। লাইফটাইম প্যারামিটার নির্দিষ্ট করার মাধ্যমে, আমরা নির্দিষ্ট করতাম যে লিস্টের প্রতিটি উপাদান অন্তত পুরো লিস্টের সমান সময়কাল বেঁচে থাকবে। Listing 15-17-এর উপাদান এবং লিস্টের ক্ষেত্রে এটি সত্য, কিন্তু সব পরিস্থিতিতে নয়।
এর পরিবর্তে, আমরা আমাদের List
-এর সংজ্ঞা পরিবর্তন করে Box<T>
-এর জায়গায় Rc<T>
ব্যবহার করব, যেমনটি Listing 15-18-এ দেখানো হয়েছে। প্রতিটি Cons
ভ্যারিয়েন্ট এখন একটি ভ্যালু এবং একটি List
-কে নির্দেশকারী একটি Rc<T>
ধারণ করবে। যখন আমরা b
তৈরি করব, তখন a
-এর মালিকানা নেওয়ার পরিবর্তে, আমরা a
-এর ধারণ করা Rc<List>
-কে ক্লোন করব, যার ফলে রেফারেন্সের সংখ্যা এক থেকে দুইয়ে বৃদ্ধি পাবে এবং a
এবং b
উভয়কেই সেই Rc<List>
-এর ডেটার মালিকানা শেয়ার করতে দেবে। আমরা c
তৈরি করার সময়ও a
কে ক্লোন করব, যার ফলে রেফারেন্সের সংখ্যা দুই থেকে তিনে বৃদ্ধি পাবে। প্রতিবার যখন আমরা Rc::clone
কল করব, Rc<List>
-এর ভেতরের ডেটার রেফারেন্স কাউন্ট বাড়বে, এবং ডেটা ততক্ষণ পর্যন্ত পরিষ্কার করা হবে না যতক্ষণ না পর্যন্ত তার রেফারেন্স সংখ্যা শূন্য হয়।
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)); }
আমাদের Rc<T>
-কে স্কোপে আনার জন্য একটি use
স্টেটমেন্ট যোগ করতে হবে কারণ এটি প্রিলিউডে (prelude) নেই। main
-এ, আমরা 5
এবং 10
ধারণকারী লিস্ট তৈরি করি এবং এটিকে a
-তে একটি নতুন Rc<List>
-এ সংরক্ষণ করি। তারপর, যখন আমরা b
এবং c
তৈরি করি, আমরা Rc::clone
ফাংশনটি কল করি এবং a
-এর Rc<List>
-এর একটি রেফারেন্স আর্গুমেন্ট হিসাবে পাস করি।
আমরা Rc::clone(&a)
-এর পরিবর্তে a.clone()
কল করতে পারতাম, কিন্তু রাস্টের কনভেনশন হলো এই ক্ষেত্রে Rc::clone
ব্যবহার করা। Rc::clone
-এর ইমপ্লিমেন্টেশন বেশিরভাগ টাইপের clone
ইমপ্লিমেন্টেশনের মতো সমস্ত ডেটার একটি ডিপ কপি (deep copy) তৈরি করে না। Rc::clone
-এর কল শুধুমাত্র রেফারেন্স কাউন্ট বাড়ায়, যা খুব বেশি সময় নেয় না। ডেটার ডিপ কপি অনেক সময় নিতে পারে। রেফারেন্স কাউন্টিংয়ের জন্য Rc::clone
ব্যবহার করে, আমরা ডিপ-কপি ধরনের ক্লোন এবং রেফারেন্স কাউন্ট বাড়ায় এমন ধরনের ক্লোনের মধ্যে দৃশ্যমানভাবে পার্থক্য করতে পারি। কোডে পারফরম্যান্স সমস্যা খোঁজার সময়, আমাদের কেবল ডিপ-কপি ক্লোনগুলো বিবেচনা করতে হবে এবং Rc::clone
-এর কলগুলোকে উপেক্ষা করা যেতে পারে।
একটি Rc<T>
ক্লোন করা রেফারেন্স কাউন্ট বাড়ায়
চলুন Listing 15-18-এর আমাদের কার্যকরী উদাহরণটি পরিবর্তন করি যাতে আমরা দেখতে পারি a
-তে থাকা Rc<List>
-এর রেফারেন্স তৈরি এবং ড্রপ করার সাথে সাথে রেফারেন্স কাউন্ট কীভাবে পরিবর্তিত হয়।
Listing 15-19-এ, আমরা main
-কে পরিবর্তন করব যাতে c
লিস্টের চারপাশে একটি অভ্যন্তরীণ স্কোপ থাকে; তাহলে আমরা দেখতে পাব c
স্কোপের বাইরে চলে গেলে রেফারেন্স কাউন্ট কীভাবে পরিবর্তিত হয়।
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; // --snip-- 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)); }
প্রোগ্রামের প্রতিটি পয়েন্টে যেখানে রেফারেন্স কাউন্ট পরিবর্তিত হয়, আমরা রেফারেন্স কাউন্ট প্রিন্ট করি, যা আমরা Rc::strong_count
ফাংশন কল করে পাই। এই ফাংশনটির নাম strong_count
কারণ Rc<T>
টাইপের একটি weak_count
-ও আছে; আমরা [“Weak<T>
ব্যবহার করে রেফারেন্স সাইকেল প্রতিরোধ করা”][preventing-ref-cycles] অংশে দেখব 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>
-এর প্রাথমিক রেফারেন্স কাউন্ট 1; তারপর প্রতিবার যখন আমরা clone
কল করি, কাউন্ট 1 করে বেড়ে যায়। যখন c
স্কোপের বাইরে চলে যায়, কাউন্ট 1 করে কমে যায়। রেফারেন্স কাউন্ট বাড়ানোর জন্য যেমন আমাদের Rc::clone
কল করতে হয়, তেমন রেফারেন্স কাউন্ট কমানোর জন্য আমাদের কোনো ফাংশন কল করতে হয় না: Drop
ট্রেইটের ইমপ্লিমেন্টেশন স্বয়ংক্রিয়ভাবে রেফারেন্স কাউন্ট কমিয়ে দেয় যখন একটি Rc<T>
ভ্যালু স্কোপের বাইরে চলে যায়।
এই উদাহরণে আমরা যা দেখতে পাচ্ছি না তা হলো, main
-এর শেষে যখন b
এবং তারপর a
স্কোপের বাইরে চলে যায়, তখন কাউন্ট 0 হয়ে যায়, এবং Rc<List>
সম্পূর্ণরূপে পরিষ্কার হয়ে যায়। Rc<T>
ব্যবহার করে একটিমাত্র ভ্যালুর একাধিক মালিক থাকতে পারে, এবং কাউন্ট নিশ্চিত করে যে ভ্যালুটি ততক্ষণ পর্যন্ত বৈধ থাকবে যতক্ষণ পর্যন্ত কোনো মালিক বিদ্যমান থাকে।
অপরিবর্তনশীল রেফারেন্সের (immutable references) মাধ্যমে, Rc<T>
আপনাকে আপনার প্রোগ্রামের একাধিক অংশের মধ্যে শুধুমাত্র পড়ার জন্য ডেটা শেয়ার করার অনুমতি দেয়। যদি Rc<T>
আপনাকে একাধিক পরিবর্তনশীল রেফারেন্সও (mutable references) রাখার অনুমতি দিত, তাহলে আপনি Chapter 4-এ আলোচিত ধার নেওয়ার নিয়মগুলোর (borrowing rules) একটি লঙ্ঘন করতে পারতেন: একই জায়গায় একাধিক পরিবর্তনশীল ধার ডেটা রেস (data races) এবং অসামঞ্জস্যের কারণ হতে পারে। কিন্তু ডেটা পরিবর্তন করতে পারা খুবই দরকারী! পরবর্তী বিভাগে, আমরা ইন্টেরিয়র মিউটেবিলিটি (interior mutability) প্যাটার্ন এবং RefCell<T>
টাইপ নিয়ে আলোচনা করব যা আপনি এই অপরিবর্তনীয়তার সীমাবদ্ধতার সাথে কাজ করার জন্য Rc<T>
-এর সাথে একত্রে ব্যবহার করতে পারেন।