struct
ব্যবহার করে একটি উদাহরণ প্রোগ্রাম
আমরা কখন struct
ব্যবহার করতে চাই তা বোঝার জন্য, আসুন একটি প্রোগ্রাম লিখি যা একটি আয়তক্ষেত্রের ক্ষেত্রফল গণনা করে। আমরা প্রথমে একক ভ্যারিয়েবল ব্যবহার করে শুরু করব, এবং তারপর প্রোগ্রামটিকে রিফ্যাক্টর (refactor) করে struct
ব্যবহার করব।
আসুন কার্গো (Cargo) দিয়ে rectangles নামে একটি নতুন বাইনারি প্রজেক্ট তৈরি করি যা পিক্সেল এককে একটি আয়তক্ষেত্রের প্রস্থ এবং উচ্চতা নিয়ে আয়তক্ষেত্রটির ক্ষেত্রফল গণনা করবে। তালিকা ৫-৮ আমাদের প্রজেক্টের src/main.rs ফাইলে ঠিক এটি করার একটি সংক্ষিপ্ত প্রোগ্রাম দেখাচ্ছে।
fn main() { let width1 = 30; let height1 = 50; println!( "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
এখন, cargo run
ব্যবহার করে এই প্রোগ্রামটি চালান:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
এই কোডটি প্রতিটি ডাইমেনশন (dimension) দিয়ে area
ফাংশনটিকে কল করে আয়তক্ষেত্রের ক্ষেত্রফল বের করতে সফল হয়েছে, কিন্তু এই কোডটিকে আরও স্পষ্ট এবং পাঠযোগ্য করার জন্য আমরা আরও অনেক কিছু করতে পারি।
এই কোডের সমস্যাটি area
ফাংশনের সিগনেচারে স্পষ্ট:
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
area
ফাংশনটির কাজ একটি আয়তক্ষেত্রের ক্ষেত্রফল গণনা করা, কিন্তু আমরা যে ফাংশনটি লিখেছি তার দুটি প্যারামিটার রয়েছে, এবং আমাদের প্রোগ্রামের কোথাও এটা স্পষ্ট নয় যে এই প্যারামিটারগুলো সম্পর্কিত। প্রস্থ এবং উচ্চতাকে একসাথে গ্রুপ করা আরও পাঠযোগ্য এবং পরিচালনাযোগ্য হবে। অধ্যায় ৩ এর "টাপল টাইপ" বিভাগে আমরা এটি করার একটি উপায় নিয়ে ইতিমধ্যে আলোচনা করেছি: টাপল (tuples) ব্যবহার করে।
টাপল দিয়ে রিফ্যাক্টরিং (Refactoring with Tuples)
তালিকা ৫-৯ আমাদের প্রোগ্রামের আরেকটি সংস্করণ দেখাচ্ছে যা টাপল ব্যবহার করে।
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
একদিক থেকে, এই প্রোগ্রামটি ভালো। টাপল আমাদের কিছুটা গঠন যোগ করতে দেয়, এবং আমরা এখন মাত্র একটি আর্গুমেন্ট পাস করছি। কিন্তু অন্যদিক থেকে, এই সংস্করণটি কম স্পষ্ট: টাপল তার উপাদানগুলোর নাম দেয় না, তাই আমাদের টাপলের অংশগুলোতে ইনডেক্স ব্যবহার করতে হয়, যা আমাদের গণনাকে কম স্পষ্ট করে তোলে।
ক্ষেত্রফল গণনার জন্য প্রস্থ এবং উচ্চতা গুলিয়ে ফেললে কোনো সমস্যা হবে না, কিন্তু যদি আমরা স্ক্রিনে আয়তক্ষেত্রটি আঁকতে চাই, তবে এটি গুরুত্বপূর্ণ হবে! আমাদের মনে রাখতে হবে যে width
হলো টাপল ইনডেক্স 0
এবং height
হলো টাপল ইনডেক্স 1
। অন্য কেউ যদি আমাদের কোড ব্যবহার করে, তবে তার জন্য এটি বোঝা এবং মনে রাখা আরও কঠিন হবে। যেহেতু আমরা আমাদের কোডে ডেটার অর্থ প্রকাশ করিনি, তাই এখন ভুল হওয়ার সম্ভাবনা বেড়ে গেছে।
struct
দিয়ে রিফ্যাক্টরিং: আরও অর্থ যোগ করা
আমরা ডেটাকে লেবেল দিয়ে অর্থ যোগ করার জন্য struct
ব্যবহার করি। আমরা যে টাপলটি ব্যবহার করছি সেটিকে একটি struct
-এ রূপান্তর করতে পারি, যেখানে পুরোটার জন্য একটি নাম এবং অংশগুলোর জন্যও নাম থাকবে, যেমনটি তালিকা ৫-১০-এ দেখানো হয়েছে।
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
এখানে, আমরা একটি struct
ডিফাইন করেছি এবং এর নাম দিয়েছি Rectangle
। কার্লি ব্র্যাকেটের ভিতরে, আমরা ফিল্ডগুলোকে width
এবং height
হিসাবে ডিফাইন করেছি, যার উভয়েরই টাইপ u32
। তারপর, main
ফাংশনে, আমরা Rectangle
-এর একটি নির্দিষ্ট ইনস্ট্যান্স তৈরি করেছি যার প্রস্থ 30
এবং উচ্চতা 50
।
আমাদের area
ফাংশনটি এখন একটি প্যারামিটার দিয়ে ডিফাইন করা হয়েছে, যার নাম আমরা দিয়েছি rectangle
, এবং এর টাইপ হলো একটি Rectangle
struct
ইনস্ট্যান্সের একটি অপরিবর্তনীয় ধার (immutable borrow)। অধ্যায় ৪-এ যেমন উল্লেখ করা হয়েছে, আমরা struct
-টির মালিকানা নেওয়ার পরিবর্তে এটি ধার করতে চাই। এইভাবে, main
তার মালিকানা ধরে রাখে এবং rect1
ব্যবহার করা চালিয়ে যেতে পারে, যে কারণে আমরা ফাংশন সিগনেচারে এবং যেখানে ফাংশনটি কল করি সেখানে &
ব্যবহার করি।
area
ফাংশনটি Rectangle
ইনস্ট্যান্সের width
এবং height
ফিল্ডগুলো অ্যাক্সেস করে (উল্লেখ্য যে একটি ধার করা struct
ইনস্ট্যান্সের ফিল্ড অ্যাক্সেস করলে ফিল্ডের মানগুলো মুভ (move) হয় না, যে কারণে আপনি প্রায়শই struct
-এর ধার দেখতে পাবেন)। আমাদের area
-এর ফাংশন সিগনেচার এখন ঠিক তাই বলছে যা আমরা বলতে চাই: Rectangle
-এর ক্ষেত্রফল গণনা করুন, এর width
এবং height
ফিল্ড ব্যবহার করে। এটি প্রকাশ করে যে প্রস্থ এবং উচ্চতা একে অপরের সাথে সম্পর্কিত, এবং এটি টাপল ইনডেক্স মান 0
এবং 1
ব্যবহার করার পরিবর্তে মানগুলোকে বর্ণনামূলক নাম দেয়। এটি স্পষ্টতার দিক থেকে একটি বড় সুবিধা।
ডিরাইভড ট্রেইট দিয়ে দরকারী কার্যকারিতা যোগ করা (Adding Useful Functionality with Derived Traits)
আমাদের প্রোগ্রাম ডিবাগ করার সময় Rectangle
-এর একটি ইনস্ট্যান্স প্রিন্ট করতে পারা এবং এর সমস্ত ফিল্ডের মান দেখতে পারা দরকারী হবে। তালিকা ৫-১১ পূর্ববর্তী অধ্যায়গুলোতে আমরা যেভাবে println!
ম্যাক্রো ব্যবহার করেছি, সেভাবে ব্যবহার করার চেষ্টা করে। তবে এটি কাজ করবে না।
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
যখন আমরা এই কোডটি কম্পাইল করি, আমরা এই মূল বার্তা সহ একটি এরর পাই:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
ম্যাক্রো অনেক ধরণের ফরম্যাটিং করতে পারে, এবং ডিফল্টভাবে, কার্লি ব্র্যাকেটগুলো println!
-কে Display
নামে পরিচিত একটি ফরম্যাটিং ব্যবহার করতে বলে: যা সরাসরি এন্ড-ইউজারের ব্যবহারের জন্য আউটপুট। আমরা এখন পর্যন্ত যে প্রিমিটিভ টাইপগুলো দেখেছি সেগুলো ডিফল্টভাবে Display
ইমপ্লিমেন্ট করে কারণ একজন ব্যবহারকারীকে 1
বা অন্য কোনো প্রিমিটিভ টাইপ দেখানোর একটিই উপায় আছে। কিন্তু struct
-এর ক্ষেত্রে, println!
আউটপুট কীভাবে ফরম্যাট করবে তা কম স্পষ্ট কারণ আরও অনেক প্রদর্শনের সম্ভাবনা রয়েছে: আপনি কি কমা চান কি না? আপনি কি কার্লি ব্র্যাকেট প্রিন্ট করতে চান? সমস্ত ফিল্ড কি দেখানো উচিত? এই অস্পষ্টতার কারণে, রাস্ট অনুমান করার চেষ্টা করে না আমরা কী চাই, এবং struct
-এর println!
এবং {}
প্লেসহোল্ডারের সাথে ব্যবহারের জন্য Display
-এর কোনো সরবরাহ করা ইমপ্লিমেন্টেশন নেই।
যদি আমরা এররগুলো পড়া চালিয়ে যাই, আমরা এই সহায়ক নোটটি খুঁজে পাব:
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead```
চলুন চেষ্টা করি! `println!` ম্যাক্রো কলটি এখন `println!("rect1 is {rect1:?}");`-এর মতো দেখাবে। কার্লি ব্র্যাকেটের ভিতরে স্পেসিফায়ার `:?` রাখলে `println!`-কে বলা হয় আমরা `Debug` নামে একটি আউটপুট ফরম্যাট ব্যবহার করতে চাই। `Debug` ট্রেইটটি আমাদের `struct`-কে এমনভাবে প্রিন্ট করতে সক্ষম করে যা ডেভেলপারদের জন্য দরকারী যাতে আমরা আমাদের কোড ডিবাগ করার সময় এর মান দেখতে পারি।
এই পরিবর্তন সহ কোডটি কম্পাইল করুন। ধুর! আমরা এখনও একটি এরর পাচ্ছি:
```text
error[E0277]: `Rectangle` doesn't implement `Debug````
কিন্তু আবারও, কম্পাইলার আমাদের একটি সহায়ক নোট দেয়:
```text
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
রাস্ট ডিবাগিং তথ্য প্রিন্ট করার জন্য কার্যকারিতা অন্তর্ভুক্ত করে, কিন্তু আমাদের struct
-এর জন্য সেই কার্যকারিতা উপলব্ধ করতে স্পষ্টভাবে অপ্ট-ইন করতে হবে। এটি করার জন্য, আমরা struct
সংজ্ঞার ঠিক আগে #[derive(Debug)]
অ্যাট্রিবিউটটি যোগ করি, যেমনটি তালিকা ৫-১২-এ দেখানো হয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
এখন যখন আমরা প্রোগ্রামটি চালাই, আমরা কোনো এরর পাব না, এবং আমরা নিম্নলিখিত আউটপুটটি দেখব:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
সুন্দর! এটি সবচেয়ে সুন্দর আউটপুট নয়, তবে এটি এই ইনস্ট্যান্সের জন্য সমস্ত ফিল্ডের মান দেখায়, যা ডিবাগিংয়ের সময় অবশ্যই সাহায্য করবে। যখন আমাদের বড় struct
থাকে, তখন এমন আউটপুট থাকা দরকারী যা পড়া একটু সহজ; সেই ক্ষেত্রে, আমরা println!
স্ট্রিং-এ {:?}
এর পরিবর্তে {:#?}
ব্যবহার করতে পারি। এই উদাহরণে, {:#?}
স্টাইল ব্যবহার করলে নিম্নলিখিত আউটপুটটি আসবে:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug
ফরম্যাট ব্যবহার করে একটি মান প্রিন্ট করার আরেকটি উপায় হলো dbg!
ম্যাক্রো ব্যবহার করা, যা একটি এক্সপ্রেশনের মালিকানা নেয় (println!
-এর বিপরীতে, যা একটি রেফারেন্স নেয়), আপনার কোডে সেই dbg!
ম্যাক্রো কলটি কোথায় ঘটেছে তার ফাইল এবং লাইন নম্বর প্রিন্ট করে এবং সেই এক্সপ্রেশনের ফলস্বরূপ মান সহ, এবং মানের মালিকানা ফেরত দেয়।
দ্রষ্টব্য:
dbg!
ম্যাক্রো কল করা স্ট্যান্ডার্ড এরর কনসোল স্ট্রিমে (stderr
) প্রিন্ট করে,println!
-এর বিপরীতে, যা স্ট্যান্ডার্ড আউটপুট কনসোল স্ট্রিমে (stdout
) প্রিন্ট করে। আমরা অধ্যায় ১২-এর "স্ট্যান্ডার্ড আউটপুটের পরিবর্তে স্ট্যান্ডার্ড এররে এরর বার্তা লেখা" বিভাগেstderr
এবংstdout
সম্পর্কে আরও কথা বলব।
এখানে একটি উদাহরণ যেখানে আমরা width
ফিল্ডে নির্ধারিত মান এবং rect1
-এর পুরো struct
-এর মানে আগ্রহী:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
আমরা 30 * scale
এক্সপ্রেশনের চারপাশে dbg!
রাখতে পারি এবং, যেহেতু dbg!
এক্সপ্রেশনের মানের মালিকানা ফেরত দেয়, width
ফিল্ডটি একই মান পাবে যেন আমাদের সেখানে dbg!
কল ছিল না। আমরা চাই না dbg!
rect1
-এর মালিকানা নিক, তাই আমরা পরবর্তী কলে rect1
-এর একটি রেফারেন্স ব্যবহার করি। এই উদাহরণের আউটপুটটি দেখতে এইরকম:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
আমরা দেখতে পাচ্ছি প্রথম আউটপুটটি src/main.rs লাইন ১০ থেকে এসেছে যেখানে আমরা 30 * scale
এক্সপ্রেশনটি ডিবাগ করছি, এবং এর ফলস্বরূপ মান 60
(পূর্ণসংখ্যার জন্য ইমপ্লিমেন্ট করা Debug
ফরম্যাটিং হলো শুধুমাত্র তাদের মান প্রিন্ট করা)। src/main.rs এর ১৪ নং লাইনের dbg!
কলটি &rect1
-এর মান আউটপুট করে, যা Rectangle
struct
। এই আউটপুটটি Rectangle
টাইপের সুন্দর Debug
ফরম্যাটিং ব্যবহার করে। dbg!
ম্যাক্রোটি খুব সহায়ক হতে পারে যখন আপনি আপনার কোড কী করছে তা বোঝার চেষ্টা করছেন!
Debug
ট্রেইট ছাড়াও, রাস্ট আমাদের derive
অ্যাট্রিবিউট দিয়ে ব্যবহারের জন্য বেশ কয়েকটি ট্রেইট সরবরাহ করেছে যা আমাদের কাস্টম টাইপগুলোতে দরকারী আচরণ যোগ করতে পারে। সেই ট্রেইট এবং তাদের আচরণগুলো পরিশিষ্ট C-তে তালিকাভুক্ত করা হয়েছে। আমরা কাস্টম আচরণ সহ এই ট্রেইটগুলো কীভাবে ইমপ্লিমেন্ট করতে হয় এবং কীভাবে আপনার নিজস্ব ট্রেইট তৈরি করতে হয় তা অধ্যায় ১০-এ আলোচনা করব। derive
ছাড়াও আরও অনেক অ্যাট্রিবিউট রয়েছে; আরও তথ্যের জন্য, রাস্ট রেফারেন্সের "অ্যাট্রিবিউটস" বিভাগটি দেখুন।
আমাদের area
ফাংশনটি খুব নির্দিষ্ট: এটি শুধুমাত্র আয়তক্ষেত্রের ক্ষেত্রফল গণনা করে। এই আচরণটিকে আমাদের Rectangle
struct
-এর সাথে আরও ঘনিষ্ঠভাবে যুক্ত করা সহায়ক হবে কারণ এটি অন্য কোনো টাইপের সাথে কাজ করবে না। আসুন দেখি কীভাবে আমরা area
ফাংশনটিকে আমাদের Rectangle
টাইপে ডিফাইন করা একটি area
মেথডে পরিণত করে এই কোডটিকে রিফ্যাক্টর করা চালিয়ে যেতে পারি।