Heap-এ ডেটা Point করার জন্য Box<T>
-এর ব্যবহার
সবচেয়ে সহজ smart pointer হলো box, যার টাইপ লেখা হয় Box<T>
। Boxes আপনাকে স্ট্যাকের (stack) পরিবর্তে হিপ-এ (heap) ডেটা সংরক্ষণ করার সুযোগ দেয়। যা স্ট্যাকে অবশিষ্ট থাকে তা হলো হিপ ডেটার একটি pointer। স্ট্যাক এবং হিপের মধ্যে পার্থক্য পর্যালোচনা করতে Chapter 4 দেখুন।
Box-এর ডেটা স্ট্যাকের পরিবর্তে হিপে সংরক্ষণ করা ছাড়া আর কোনো পারফরম্যান্স ওভারহেড নেই। তবে এদের খুব বেশি অতিরিক্ত ক্ষমতাও নেই। আপনি বেশিরভাগ সময়ে এই পরিস্থিতিগুলিতে এগুলি ব্যবহার করবেন:
- যখন আপনার কাছে এমন একটি টাইপ থাকে যার সাইজ কম্পাইল টাইমে জানা যায় না এবং আপনি সেই টাইপের একটি ভ্যালু এমন একটি কনটেক্সট-এ ব্যবহার করতে চান যেখানে একটি নির্দিষ্ট সাইজের প্রয়োজন হয়।
- যখন আপনার কাছে প্রচুর পরিমাণে ডেটা থাকে এবং আপনি ownership হস্তান্তর করতে চান কিন্তু নিশ্চিত করতে চান যে ডেটা কপি হবে না।
- যখন আপনি একটি ভ্যালুর owner হতে চান এবং আপনি শুধু চান যে এটি একটি নির্দিষ্ট ট্রেইট (trait) ইমপ্লিমেন্ট করে, কোনো নির্দিষ্ট টাইপের না হয়ে।
আমরা প্রথম পরিস্থিতিটি দেখাব "Recursive Types with Boxes" অংশে। দ্বিতীয় ক্ষেত্রে, বিপুল পরিমাণ ডেটার ownership হস্তান্তর করতে অনেক সময় লাগতে পারে কারণ ডেটা স্ট্যাকের উপর কপি করা হয়। এই পরিস্থিতিতে পারফরম্যান্স উন্নত করতে, আমরা বিপুল পরিমাণ ডেটা একটি box-এ করে হিপ-এ সংরক্ষণ করতে পারি। তারপরে, শুধুমাত্র অল্প পরিমাণ pointer ডেটা স্ট্যাকের উপর কপি করা হয়, যখন এটি যে ডেটাকে নির্দেশ করে তা হিপের এক জায়গায় থাকে। তৃতীয় ক্ষেত্রটি একটি trait object হিসাবে পরিচিত, এবং Chapter 18-এর ["Using Trait Objects That Allow for Values of Different Types,"][trait-objects] অংশটি এই বিষয়ে উৎসর্গীকৃত। সুতরাং আপনি এখানে যা শিখবেন তা সেই অংশে আবার প্রয়োগ করবেন!
Heap-এ ডেটা সংরক্ষণের জন্য Box<T>
ব্যবহার করা
Box<T>
-এর হিপ স্টোরেজ ব্যবহার নিয়ে আলোচনা করার আগে, আমরা এর সিনট্যাক্স এবং Box<T>
-এর মধ্যে সংরক্ষিত ভ্যালুগুলোর সাথে কীভাবে ইন্টারঅ্যাক্ট করতে হয় তা দেখব।
Listing 15-1 দেখাচ্ছে কীভাবে একটি box ব্যবহার করে একটি i32
ভ্যালু হিপ-এ সংরক্ষণ করা যায়।
fn main() { let b = Box::new(5); println!("b = {b}"); }
আমরা b
ভেরিয়েবলটিকে 5
ভ্যালুর একটি Box
-এর মান হিসাবে সংজ্ঞায়িত করি, যা হিপ-এ allocate করা হয়েছে। এই প্রোগ্রামটি b = 5
প্রিন্ট করবে; এক্ষেত্রে, আমরা box-এর ডেটা অ্যাক্সেস করতে পারি ঠিক সেভাবে যেভাবে আমরা করতাম যদি এই ডেটা স্ট্যাকে থাকতো। যেকোনো owned ভ্যালুর মতোই, যখন একটি box স্কোপের বাইরে চলে যায়, যেমন b
main
-এর শেষে চলে যাচ্ছে, এটি ডিঅ্যালোকেট (deallocated) হয়ে যাবে। ডিঅ্যালোকেশনটি box (যা স্ট্যাকে সংরক্ষিত) এবং এটি যে ডেটাকে নির্দেশ করে (যা হিপ-এ সংরক্ষিত) উভয়ের জন্যই ঘটে।
হিপ-এ একটিমাত্র ভ্যালু রাখা খুব একটা কাজের না, তাই আপনি সাধারণত এভাবে box ব্যবহার করবেন না। বেশিরভাগ পরিস্থিতিতে, একটি i32
এর মতো ভ্যালু স্ট্যাকের উপর রাখাই বেশি উপযুক্ত, যেখানে ডিফল্টভাবে সেগুলি সংরক্ষণ করা হয়। চলুন এমন একটি ক্ষেত্র দেখি যেখানে box আমাদের এমন টাইপ সংজ্ঞায়িত করার অনুমতি দেয় যা box ছাড়া আমরা সংজ্ঞায়িত করতে পারতাম না।
Box ব্যবহার করে Recursive Type সক্রিয় করা
একটি recursive type-এর ভ্যালু নিজের একটি অংশ হিসেবে একই টাইপের আরেকটি ভ্যালু রাখতে পারে। Recursive type একটি সমস্যা তৈরি করে কারণ রাস্টকে কম্পাইল টাইমে জানতে হয় একটি টাইপ কতটুকু জায়গা নেয়। কিন্তু, recursive type-এর ভ্যালুগুলোর নেস্টিং (nesting) তাত্ত্বিকভাবে অসীম পর্যন্ত চলতে পারে, তাই রাস্ট জানতে পারে না ভ্যালুটির জন্য কতটুকু জায়গা প্রয়োজন। যেহেতু box-এর একটি নির্দিষ্ট সাইজ আছে, তাই আমরা recursive type-এর সংজ্ঞায় একটি box যোগ করে recursive type সক্রিয় করতে পারি।
একটি recursive type-এর উদাহরণ হিসেবে, আসুন আমরা cons list দেখি। এটি একটি ডেটা টাইপ যা সাধারণত ফাংশনাল প্রোগ্রামিং ভাষায় পাওয়া যায়। আমরা যে cons list টাইপটি সংজ্ঞায়িত করব তা recursion ছাড়া খুবই সহজ; তাই, আমরা যে উদাহরণটি নিয়ে কাজ করব তার ধারণাগুলো যেকোনো সময় যখন আপনি recursive type জড়িত আরও জটিল পরিস্থিতিতে পড়বেন তখন কার্যকর হবে।
Cons List সম্পর্কে আরও তথ্য
একটি cons list হলো একটি ডেটা স্ট্রাকচার যা Lisp প্রোগ্রামিং ভাষা এবং এর উপভাষা থেকে এসেছে, এটি নেস্টেড পেয়ার (nested pairs) দ্বারা গঠিত এবং এটি লিঙ্কড লিস্টের (linked list) Lisp সংস্করণ। এর নাম Lisp-এর cons
ফাংশন (যা construct function-এর সংক্ষিপ্ত রূপ) থেকে এসেছে, যা তার দুটি আর্গুমেন্ট থেকে একটি নতুন পেয়ার তৈরি করে। একটি ভ্যালু এবং আরেকটি পেয়ার নিয়ে গঠিত একটি পেয়ারের উপর cons
কল করে, আমরা রিকার্সিভ পেয়ার দ্বারা গঠিত cons list তৈরি করতে পারি।
উদাহরণস্বরূপ, এখানে 1, 2, 3
লিস্ট ধারণকারী একটি cons list-এর একটি स्यूडोकोड (pseudocode) উপস্থাপনা রয়েছে, যেখানে প্রতিটি পেয়ার বন্ধনীতে রয়েছে:
(1, (2, (3, Nil)))
একটি cons list-এর প্রতিটি আইটেমে দুটি উপাদান থাকে: বর্তমান আইটেমের ভ্যালু এবং পরবর্তী আইটেম। লিস্টের শেষ আইটেমে শুধু Nil
নামক একটি ভ্যালু থাকে এবং কোনো পরবর্তী আইটেম থাকে না। একটি cons list রিকার্সিভভাবে cons
ফাংশন কল করে তৈরি করা হয়। রিকার্সনের বেস কেস (base case) বোঝানোর জন্য প্রমিত নাম হল Nil
। মনে রাখবেন যে এটি Chapter 6-এ আলোচিত "null" বা "nil" ধারণার মতো নয়, যা একটি অবৈধ বা অনুপস্থিত ভ্যালু।
Cons list রাস্ট-এ একটি সাধারণভাবে ব্যবহৃত ডেটা স্ট্রাকচার নয়। রাস্ট-এ যখন আপনার কাছে আইটেমের একটি তালিকা থাকে, তখন Vec<T>
ব্যবহার করা একটি ভালো পছন্দ। অন্যান্য, আরও জটিল রিকার্সিভ ডেটা টাইপ বিভিন্ন পরিস্থিতিতে কার্যকর, কিন্তু এই অধ্যায়ে cons list দিয়ে শুরু করে, আমরা দেখতে পারি কীভাবে box আমাদের খুব বেশি বিভ্রান্তি ছাড়াই একটি রিকার্সিভ ডেটা টাইপ সংজ্ঞায়িত করতে দেয়।
Listing 15-2-তে একটি cons list-এর জন্য একটি enum সংজ্ঞা রয়েছে। মনে রাখবেন যে এই কোডটি এখনও কম্পাইল হবে না কারণ List
টাইপের কোনো নির্দিষ্ট সাইজ নেই, যা আমরা দেখাব।
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
দ্রষ্টব্য: আমরা এই উদাহরণের উদ্দেশ্যে কেবল
i32
ভ্যালু ধারণকারী একটি cons list ইমপ্লিমেন্ট করছি। আমরা Chapter 10-এ আলোচনা করা জেনেরিক ব্যবহার করে এটি ইমপ্লিমেন্ট করতে পারতাম, যাতে যেকোনো টাইপের ভ্যালু সংরক্ষণ করতে পারে এমন একটি cons list টাইপ সংজ্ঞায়িত করা যায়।
1, 2, 3
লিস্ট সংরক্ষণ করার জন্য List
টাইপ ব্যবহার করা Listing 15-3-এর কোডের মতো দেখাবে।
enum List {
Cons(i32, List),
Nil,
}
// --snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
প্রথম Cons
ভ্যালুটি 1
এবং আরেকটি List
ভ্যালু ধারণ করে। এই List
ভ্যালুটি আরেকটি Cons
ভ্যালু যা 2
এবং আরেকটি List
ভ্যালু ধারণ করে। এই List
ভ্যালুটি আরও একটি Cons
ভ্যালু যা 3
এবং একটি List
ভ্যালু ধারণ করে, যা অবশেষে Nil
, নন-রিকার্সিভ ভ্যারিয়েন্ট যা লিস্টের সমাপ্তি নির্দেশ করে।
যদি আমরা Listing 15-3-এর কোডটি কম্পাইল করার চেষ্টা করি, আমরা Listing 15-4-এ দেখানো এররটি পাই।
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing when `List` needs drop
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
এররটি দেখায় যে এই টাইপের "সাইজ অসীম"। কারণ হল আমরা List
-কে একটি ভ্যারিয়েন্ট দিয়ে সংজ্ঞায়িত করেছি যা রিকার্সিভ: এটি সরাসরি নিজের আরেকটি ভ্যালু ধারণ করে। ফলস্বরূপ, রাস্ট বের করতে পারে না যে একটি List
ভ্যালু সংরক্ষণ করতে তার কতটুকু জায়গা প্রয়োজন। আসুন আমরা ভেঙে দেখি কেন আমরা এই এররটি পাই। প্রথমে আমরা দেখব কীভাবে রাস্ট সিদ্ধান্ত নেয় যে একটি নন-রিকার্সিভ টাইপের ভ্যালু সংরক্ষণ করতে তার কতটুকু জায়গা প্রয়োজন।
একটি নন-রিকার্সিভ টাইপের সাইজ গণনা করা
স্মরণ করুন Chapter 6-এ enum সংজ্ঞা নিয়ে আলোচনা করার সময় আমরা Listing 6-2-তে যে Message
enum সংজ্ঞায়িত করেছিলাম:
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
একটি Message
ভ্যালুর জন্য কতটুকু জায়গা বরাদ্দ করতে হবে তা নির্ধারণ করতে, রাস্ট প্রতিটি ভ্যারিয়েন্টের মধ্যে দিয়ে যায় তা দেখতে কোন ভ্যারিয়েন্টের সবচেয়ে বেশি জায়গা প্রয়োজন। রাস্ট দেখে যে Message::Quit
-এর কোনো জায়গার প্রয়োজন নেই, Message::Move
-এর দুটি i32
ভ্যালু সংরক্ষণ করার জন্য যথেষ্ট জায়গার প্রয়োজন, এবং আরও অনেক কিছু। যেহেতু কেবল একটি ভ্যারিয়েন্ট ব্যবহার করা হবে, একটি Message
ভ্যালুর জন্য সর্বাধিক যে জায়গার প্রয়োজন হবে তা হল এর বৃহত্তম ভ্যারিয়েন্টটি সংরক্ষণ করতে যে জায়গা লাগবে।
এর সাথে তুলনা করুন কী ঘটে যখন রাস্ট Listing 15-2-এর List
enum-এর মতো একটি রিকার্সিভ টাইপের জন্য কতটুকু জায়গা প্রয়োজন তা নির্ধারণ করার চেষ্টা করে। কম্পাইলার Cons
ভ্যারিয়েন্টটি দেখে শুরু করে, যা i32
টাইপের একটি ভ্যালু এবং List
টাইপের একটি ভ্যালু ধারণ করে। অতএব, Cons
-এর জন্য একটি i32
-এর সাইজ এবং একটি List
-এর সাইজের সমান জায়গার প্রয়োজন। List
টাইপের জন্য কত মেমরি প্রয়োজন তা বের করতে, কম্পাইলার ভ্যারিয়েন্টগুলো দেখে, Cons
ভ্যারিয়েন্ট দিয়ে শুরু করে। Cons
ভ্যারিয়েন্ট i32
টাইপের একটি ভ্যালু এবং List
টাইপের একটি ভ্যালু ধারণ করে, এবং এই প্রক্রিয়াটি অসীমভাবে চলতে থাকে, যেমনটি Figure 15-1-এ দেখানো হয়েছে।
Figure 15-1: অসীম Cons
ভ্যারিয়েন্ট নিয়ে গঠিত একটি অসীম List
একটি নির্দিষ্ট সাইজের রিকার্সিভ টাইপ পেতে Box<T>
ব্যবহার করা
যেহেতু রাস্ট রিকার্সিভভাবে সংজ্ঞায়িত টাইপের জন্য কতটুকু জায়গা বরাদ্দ করতে হবে তা বের করতে পারে না, তাই কম্পাইলার এই সহায়ক পরামর্শ সহ একটি এরর দেয়:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
এই পরামর্শে, indirection মানে হল একটি ভ্যালু সরাসরি সংরক্ষণ করার পরিবর্তে, আমাদের ডেটা স্ট্রাকচারটি পরিবর্তন করে ভ্যালুটির একটি pointer সংরক্ষণ করে পরোক্ষভাবে ভ্যালুটি সংরক্ষণ করা উচিত।
যেহেতু একটি Box<T>
একটি pointer, রাস্ট সবসময় জানে একটি Box<T>
-এর জন্য কতটুকু জায়গা প্রয়োজন: একটি pointer-এর সাইজ এটি যে পরিমাণ ডেটাকে নির্দেশ করছে তার উপর ভিত্তি করে পরিবর্তন হয় না। এর মানে হল আমরা Cons
ভ্যারিয়েন্টের ভিতরে সরাসরি আরেকটি List
ভ্যালুর পরিবর্তে একটি Box<T>
রাখতে পারি। Box<T>
পরবর্তী List
ভ্যালুটিকে নির্দেশ করবে যা Cons
ভ্যারিয়েন্টের ভিতরে না থেকে হিপ-এ থাকবে। ধারণাগতভাবে, আমাদের এখনও একটি লিস্ট আছে, যা অন্য লিস্ট ধারণকারী লিস্ট দিয়ে তৈরি, কিন্তু এই ইমপ্লিমেন্টেশনটি এখন আইটেমগুলোকে একে অপরের ভিতরে রাখার চেয়ে একে অপরের পাশে রাখার মতো।
আমরা Listing 15-2-এর List
enum-এর সংজ্ঞা এবং Listing 15-3-এর List
-এর ব্যবহার পরিবর্তন করে Listing 15-5-এর কোডে পরিণত করতে পারি, যা কম্পাইল হবে।
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
Cons
ভ্যারিয়েন্টের জন্য একটি i32
-এর সাইজ এবং box-এর pointer ডেটা সংরক্ষণ করার জন্য জায়গার প্রয়োজন। Nil
ভ্যারিয়েন্ট কোনো ভ্যালু সংরক্ষণ করে না, তাই এটির জন্য Cons
ভ্যারিয়েন্টের চেয়ে স্ট্যাক-এ কম জায়গা প্রয়োজন। আমরা এখন জানি যে কোনো List
ভ্যালু একটি i32
-এর সাইজ এবং একটি box-এর pointer ডেটার সাইজ গ্রহণ করবে। একটি box ব্যবহার করে, আমরা অসীম, রিকার্সিভ চেইনটি ভেঙে দিয়েছি, তাই কম্পাইলার একটি List
ভ্যালু সংরক্ষণ করার জন্য প্রয়োজনীয় সাইজ বের করতে পারে। Figure 15-2 দেখাচ্ছে Cons
ভ্যারিয়েন্টটি এখন কেমন দেখায়।
Figure 15-2: একটি List
যা অসীম আকারের নয় কারণ Cons
একটি Box
ধারণ করে
Box শুধুমাত্র indirection এবং heap allocation প্রদান করে; তাদের অন্য কোনো বিশেষ ক্ষমতা নেই, যেমনটি আমরা অন্যান্য smart pointer টাইপের সাথে দেখব। তাদের সেই বিশেষ ক্ষমতাগুলির কারণে যে পারফরম্যান্স ওভারহেড হয় তাও তাদের নেই, তাই তারা cons list-এর মতো ক্ষেত্রে উপযোগী হতে পারে যেখানে indirection-ই আমাদের একমাত্র প্রয়োজন। আমরা Chapter 18-এ box-এর আরও ব্যবহারের ক্ষেত্র দেখব।
Box<T>
টাইপটি একটি smart pointer কারণ এটি Deref
ট্রেইট ইমপ্লিমেন্ট করে, যা Box<T>
ভ্যালুগুলোকে reference-এর মতো ব্যবহার করার অনুমতি দেয়। যখন একটি Box<T>
ভ্যালু স্কোপের বাইরে চলে যায়, তখন box যে হিপ ডেটাকে নির্দেশ করছে সেটিও Drop
ট্রেইট ইমপ্লিমেন্টেশনের কারণে পরিষ্কার হয়ে যায়। এই দুটি ট্রেইট এই অধ্যায়ের বাকি অংশে আমরা যে অন্যান্য smart pointer টাইপগুলো নিয়ে আলোচনা করব তাদের কার্যকারিতার জন্য আরও বেশি গুরুত্বপূর্ণ হবে। আসুন আমরা এই দুটি ট্রেইট আরও বিস্তারিতভাবে দেখি।