স্ট্রাকট ব্যবহার করে একটি উদাহরণ প্রোগ্রাম (An Example Program Using Structs)

কখন আমরা স্ট্রাকট ব্যবহার করতে চাইতে পারি তা বোঝার জন্য, আসুন একটি প্রোগ্রাম লিখি যা একটি আয়তক্ষেত্রের ক্ষেত্রফল গণনা করে। আমরা প্রথমে আলাদা ভেরিয়েবল ব্যবহার করে শুরু করব, এবং তারপর স্ট্রাকট ব্যবহার না করা পর্যন্ত প্রোগ্রামটিকে রিফ্যাক্টর (refactor) করব।

আসুন, Cargo দিয়ে rectangles নামে একটি নতুন বাইনারি প্রোজেক্ট তৈরি করি, যেটি পিক্সেলের সাপেক্ষে একটি আয়তক্ষেত্রের প্রস্থ এবং উচ্চতা নেবে এবং আয়তক্ষেত্রটির ক্ষেত্রফল গণনা করবে। Listing 5-8 আমাদের প্রোজেক্টের 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 ফাংশনটি একটি আয়তক্ষেত্রের ক্ষেত্রফল গণনা করার কথা, কিন্তু আমরা যে ফাংশনটি লিখেছি তাতে দুটি প্যারামিটার রয়েছে এবং আমাদের প্রোগ্রামে এটি কোথাও স্পষ্ট নয় যে প্যারামিটারগুলো সম্পর্কিত। প্রস্থ এবং উচ্চতাকে একসাথে গ্রুপ করা আরও পাঠযোগ্য এবং পরিচালনাযোগ্য হবে। আমরা ইতিমধ্যেই চ্যাপ্টার ৩-এর “টাপল টাইপ” বিভাগে এটি করার একটি উপায় নিয়ে আলোচনা করেছি: টাপল ব্যবহার করে।

টাপল দিয়ে রিফ্যাক্টরিং (Refactoring with Tuples)

Listing 5-9 আমাদের প্রোগ্রামের আরেকটি ভার্সন দেখায় যা টাপল ব্যবহার করে।

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। অন্য কারও জন্য এটি বোঝা এবং মনে রাখা আরও কঠিন হবে যদি তারা আমাদের কোড ব্যবহার করে। যেহেতু আমরা আমাদের কোডে আমাদের ডেটার অর্থ প্রকাশ করিনি, তাই এখন এরর আনা সহজ।

স্ট্রাকট দিয়ে রিফ্যাক্টরিং: আরও অর্থ যোগ করা (Refactoring with Structs: Adding More Meaning)

আমরা ডেটাকে লেবেল করে অর্থ যোগ করতে স্ট্রাকট ব্যবহার করি। আমরা যে টাপলটি ব্যবহার করছি সেটিকে আমরা সম্পূর্ণ অংশের জন্য একটি নাম এবং অংশগুলোর জন্য নাম সহ একটি স্ট্রাকটে রূপান্তর করতে পারি, যেমনটি Listing 5-10-এ দেখানো হয়েছে।

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
}

এখানে আমরা একটি স্ট্রাকট সংজ্ঞায়িত করেছি এবং এর নাম দিয়েছি Rectangle। কার্লি ব্র্যাকেটের ভিতরে, আমরা ফিল্ডগুলোকে width এবং height হিসাবে সংজ্ঞায়িত করেছি, যেগুলোর উভয়ের টাইপ u32। তারপর, main-এ, আমরা Rectangle-এর একটি নির্দিষ্ট ইন্সট্যান্স তৈরি করেছি যার প্রস্থ 30 এবং উচ্চতা 50

আমাদের area ফাংশনটি এখন একটি প্যারামিটার দিয়ে সংজ্ঞায়িত করা হয়েছে, যার নাম আমরা দিয়েছি rectangle, যার টাইপ হল একটি স্ট্রাকট Rectangle ইন্সট্যান্সের ইমিউটেবল বোরো। চ্যাপ্টার ৪-এ যেমন উল্লেখ করা হয়েছে, আমরা স্ট্রাকটটির ওনারশিপ নেওয়ার পরিবর্তে বোরো করতে চাই। এইভাবে, main তার ওনারশিপ বজায় রাখে এবং rect1 ব্যবহার করা চালিয়ে যেতে পারে, যে কারণে আমরা ফাংশন সিগনেচারে এবং যেখানে আমরা ফাংশনটি কল করি সেখানে & ব্যবহার করি।

area ফাংশনটি Rectangle ইন্সট্যান্সের width এবং height ফিল্ড অ্যাক্সেস করে (মনে রাখবেন যে একটি ধার করা স্ট্রাকট ইন্সট্যান্সের ফিল্ডগুলো অ্যাক্সেস করলে ফিল্ডের মানগুলো সরানো হয় না, যে কারণে আপনি প্রায়শই স্ট্রাকটগুলোর বোরো দেখতে পান)। area-এর জন্য আমাদের ফাংশন সিগনেচারটি এখন ঠিক সেটাই বলে যা আমরা বোঝাতে চাই: Rectangle-এর ক্ষেত্রফল গণনা করুন, এর width এবং height ফিল্ড ব্যবহার করে। এটি প্রকাশ করে যে প্রস্থ এবং উচ্চতা একে অপরের সাথে সম্পর্কিত এবং এটি 0 এবং 1-এর টাপল ইনডেক্স মানগুলো ব্যবহার করার পরিবর্তে মানগুলোকে বর্ণনামূলক নাম দেয়। এটি স্পষ্টতার জন্য একটি জয়।

ডিরাইভড ট্রেইট দিয়ে দরকারী কার্যকারিতা যোগ করা (Adding Useful Functionality with Derived Traits)

আমরা যখন আমাদের প্রোগ্রাম ডিবাগ করছি তখন Rectangle-এর একটি ইন্সট্যান্স প্রিন্ট করতে এবং এর সমস্ত ফিল্ডের মান দেখতে পারা দরকারী হবে। Listing 5-11 আগের চ্যাপ্টারগুলোতে ব্যবহৃত 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 বা অন্য কোনো প্রিমিটিভ টাইপ দেখানোর একটিমাত্র উপায় রয়েছে। কিন্তু স্ট্রাকটগুলোর সাথে, println! কীভাবে আউটপুট ফরম্যাট করবে তা কম স্পষ্ট কারণ আরও প্রদর্শনের সম্ভাবনা রয়েছে: আপনি কি কমা চান নাকি চান না? আপনি কি কার্লি ব্র্যাকেটগুলো প্রিন্ট করতে চান? সমস্ত ফিল্ড দেখানো উচিত? এই অস্পষ্টতার কারণে, Rust আমরা কী চাই তা অনুমান করার চেষ্টা করে না এবং স্ট্রাকটগুলোতে 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 ট্রেইটটি আমাদের স্ট্রাকটটিকে এমনভাবে প্রিন্ট করতে সক্ষম করে যা ডেভেলপারদের জন্য দরকারী, যাতে আমরা আমাদের কোড ডিবাগ করার সময় এর মান দেখতে পারি।

এই পরিবর্তন সহ কোড কম্পাইল করুন। ধুর! আমরা এখনও একটি এরর পাই:

error[E0277]: `Rectangle` doesn't implement `Debug`

কিন্তু আবারও, কম্পাইলার আমাদের একটি সহায়ক নোট দেয়:

   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

Rust-এ ডিবাগিং তথ্য প্রিন্ট করার কার্যকারিতা অন্তর্ভুক্ত, কিন্তু আমাদের স্ট্রাকটের জন্য সেই কার্যকারিতা উপলব্ধ করতে স্পষ্টভাবে অপ্ট ইন করতে হবে। সেটি করার জন্য, আমরা স্ট্রাকট সংজ্ঞার ঠিক আগে আউটার অ্যাট্রিবিউট #[derive(Debug)] যোগ করি, যেমনটি Listing 5-12-তে দেখানো হয়েছে।

#[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 }

দারুণ! এটি সবচেয়ে সুন্দর আউটপুট নয়, তবে এটি এই ইন্সট্যান্সের সমস্ত ফিল্ডের মান দেখায়, যা অবশ্যই ডিবাগিংয়ের সময় সাহায্য করবে। যখন আমাদের কাছে বড় স্ট্রাকট থাকে, তখন এমন আউটপুট পাওয়া দরকারী যা পড়া একটু সহজ; সেই ক্ষেত্রগুলোতে, আমরা 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-এর সম্পূর্ণ স্ট্রাকটের মান জানতে আগ্রহী:

#[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 স্ট্রাকট। এই আউটপুটটি Rectangle টাইপের প্রিটি Debug ফরম্যাটিং ব্যবহার করে। আপনার কোড কী করছে তা বোঝার চেষ্টা করার সময় dbg! ম্যাক্রো সত্যিই সহায়ক হতে পারে!

Debug ট্রেইট ছাড়াও, Rust আমাদের derive অ্যাট্রিবিউট দিয়ে ব্যবহার করার জন্য বেশ কয়েকটি ট্রেইট সরবরাহ করেছে যা আমাদের কাস্টম টাইপগুলোতে দরকারী আচরণ যোগ করতে পারে। সেই ট্রেইটগুলো এবং তাদের আচরণগুলো Appendix C-তে তালিকাভুক্ত করা হয়েছে। আমরা চ্যাপ্টার ১০-এ কাস্টম আচরণ সহ এই ট্রেইটগুলো কীভাবে ইমপ্লিমেন্ট করতে হয় এবং সেইসাথে কীভাবে আপনার নিজের ট্রেইট তৈরি করতে হয় তা কভার করব। derive ছাড়াও আরও অনেক অ্যাট্রিবিউট রয়েছে; আরও তথ্যের জন্য, Rust রেফারেন্সের “অ্যাট্রিবিউটস” বিভাগ দেখুন।

আমাদের area ফাংশনটি খুব নির্দিষ্ট: এটি শুধুমাত্র আয়তক্ষেত্রের ক্ষেত্রফল গণনা করে। এই আচরণটিকে আমাদের Rectangle স্ট্রাকটের সাথে আরও ঘনিষ্ঠভাবে যুক্ত করা সহায়ক হবে কারণ এটি অন্য কোনো টাইপের সাথে কাজ করবে না। চলুন দেখি কিভাবে আমরা এই কোডটিকে রিফ্যাক্টর করে area ফাংশনকে আমাদের Rectangle টাইপে সংজ্ঞায়িত একটি area মেথডে পরিণত করতে পারি।