Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Vector ব্যবহার করে ভ্যালুর তালিকা স্টোর করা

আমরা প্রথম যে collection টাইপটি দেখব তা হলো Vec<T>, যা vector নামেও পরিচিত। Vector আপনাকে একটি ডেটা স্ট্রাকচারের মধ্যে একাধিক ভ্যালু স্টোর করার সুযোগ দেয়, যা মেমরিতে সমস্ত ভ্যালু একে অপরের পাশে রাখে। Vector শুধুমাত্র একই টাইপের ভ্যালু স্টোর করতে পারে। যখন আপনার কাছে আইটেমের একটি তালিকা থাকে, যেমন একটি ফাইলের টেক্সট লাইন বা শপিং কার্টে থাকা আইটেমের দাম, তখন এগুলি খুব দরকারি।

নতুন Vector তৈরি করা

একটি নতুন খালি vector তৈরি করতে, আমরা Vec::new ফাংশনটি কল করি, যেমনটি লিস্টিং ৮-১ এ দেখানো হয়েছে।

fn main() {
    let v: Vec<i32> = Vec::new();
}

লক্ষ্য করুন, আমরা এখানে একটি type annotation যোগ করেছি। যেহেতু আমরা এই vector-এ কোনো ভ্যালু রাখছি না, তাই Rust জানে না আমরা কী ধরনের element স্টোর করতে চাই। এটি একটি গুরুত্বপূর্ণ বিষয়। Vector জেনেরিক (generics) ব্যবহার করে তৈরি করা হয়; আমরা অধ্যায় ১০-এ আপনার নিজের টাইপের সাথে জেনেরিক কীভাবে ব্যবহার করতে হয় তা আলোচনা করব। আপাতত জেনে রাখুন যে standard library দ্বারা সরবরাহ করা Vec<T> টাইপটি যেকোনো টাইপ ধারণ করতে পারে। যখন আমরা একটি নির্দিষ্ট টাইপের ভ্যালু রাখার জন্য একটি vector তৈরি করি, তখন আমরা angle brackets-এর মধ্যে টাইপটি নির্দিষ্ট করতে পারি। লিস্টিং ৮-১ এ, আমরা Rust-কে বলেছি যে v-তে থাকা Vec<T> টি i32 টাইপের element ধারণ করবে।

বেশিরভাগ সময়, আপনি প্রাথমিক ভ্যালুসহ একটি Vec<T> তৈরি করবেন এবং Rust অনুমান করে নেবে আপনি কোন টাইপের ভ্যালু স্টোর করতে চান, তাই আপনাকে খুব কমই এই type annotation করতে হবে। Rust সুবিধাজনকভাবে vec! macro সরবরাহ করে, যা আপনার দেওয়া ভ্যালুগুলো ধারণ করে একটি নতুন vector তৈরি করবে। লিস্টিং ৮-২ একটি নতুন Vec<i32> তৈরি করে যা 1, 2, এবং 3 ভ্যালুগুলো ধারণ করে। Integer টাইপটি i32 কারণ এটি ডিফল্ট ইন্টিজার টাইপ, যা আমরা অধ্যায় ৩ এর "Data Types" বিভাগে আলোচনা করেছি।

fn main() {
    let v = vec![1, 2, 3];
}

যেহেতু আমরা প্রাথমিক i32 ভ্যালু দিয়েছি, Rust অনুমান করতে পারে যে v-এর টাইপ হলো Vec<i32>, এবং type annotation-এর প্রয়োজন নেই। এরপর, আমরা দেখব কীভাবে একটি vector পরিবর্তন করতে হয়।

Vector আপডেট করা

একটি vector তৈরি করে তাতে element যোগ করার জন্য, আমরা push মেথড ব্যবহার করতে পারি, যেমনটি লিস্টিং ৮-৩ এ দেখানো হয়েছে।

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

যেকোনো ভ্যারিয়েবলের মতোই, যদি আমরা এর ভ্যালু পরিবর্তন করতে চাই, তবে অধ্যায় ৩-এ আলোচিত mut কীওয়ার্ড ব্যবহার করে এটিকে mutable করতে হবে। আমরা যে সংখ্যাগুলো এর ভেতরে রেখেছি তা সবই i32 টাইপের, এবং Rust ডেটা থেকে এটি অনুমান করে নেয়, তাই আমাদের Vec<i32> annotation-এর প্রয়োজন নেই।

Vector-এর Element পড়া

একটি vector-এ স্টোর করা ভ্যালু রেফারেন্স করার দুটি উপায় আছে: indexing ব্যবহার করে অথবা get মেথড ব্যবহার করে। নীচের উদাহরণগুলিতে, আমরা অতিরিক্ত স্পষ্টতার জন্য এই ফাংশনগুলো থেকে ফেরত আসা ভ্যালুগুলির টাইপ annotate করেছি।

লিস্টিং ৮-৪ একটি vector-এর ভ্যালু অ্যাক্সেস করার উভয় পদ্ধতি দেখায়, indexing সিনট্যাক্স এবং get মেথডসহ।

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}

এখানে কয়েকটি বিষয় লক্ষ্য করুন। আমরা তৃতীয় element পেতে index ভ্যালু 2 ব্যবহার করি কারণ vector শূন্য থেকে সংখ্যা দ্বারা index করা হয়। & এবং [] ব্যবহার করে আমরা index-এর element-এর একটি রেফারেন্স পাই। যখন আমরা get মেথডটি আর্গুমেন্ট হিসাবে index পাস করে ব্যবহার করি, তখন আমরা একটি Option<&T> পাই যা আমরা match-এর সাথে ব্যবহার করতে পারি।

Rust একটি element রেফারেন্স করার এই দুটি উপায় সরবরাহ করে যাতে আপনি বেছে নিতে পারেন যে প্রোগ্রামটি কীভাবে আচরণ করবে যখন আপনি বিদ্যমান element-এর পরিসরের বাইরের কোনো index ভ্যালু ব্যবহার করার চেষ্টা করবেন। উদাহরণস্বরূপ, ধরা যাক আমাদের পাঁচটি element-এর একটি vector আছে এবং আমরা প্রতিটি কৌশল ব্যবহার করে ১০০তম index-এর একটি element অ্যাক্সেস করার চেষ্টা করি, যেমনটি লিস্টিং ৮-৫ এ দেখানো হয়েছে।

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}```

</Listing>

যখন আমরা এই কোডটি চালাই, প্রথম `[]` মেথডটি প্রোগ্রামটিকে প্যানিক (panic) করাবে কারণ এটি একটি অস্তিত্বহীন element-কে রেফারেন্স করছে। এই পদ্ধতিটি তখনই সবচেয়ে ভালো যখন আপনি চান যে আপনার প্রোগ্রামটি ক্র্যাশ করুক যদি vector-এর শেষের বাইরে কোনো element অ্যাক্সেস করার চেষ্টা করা হয়।

যখন `get` মেথডটিকে এমন একটি index পাস করা হয় যা vector-এর বাইরে, তখন এটি প্যানিক না করে `None` রিটার্ন করে। আপনি এই পদ্ধতিটি ব্যবহার করবেন যদি vector-এর পরিসরের বাইরের কোনো element অ্যাক্সেস করা সাধারণ পরিস্থিতিতে মাঝে মাঝে ঘটতে পারে। আপনার কোডে তখন `Some(&element)` বা `None` পরিচালনা করার জন্য লজিক থাকবে, যেমনটি অধ্যায় ৬-এ আলোচনা করা হয়েছে। উদাহরণস্বরূপ, indexটি কোনো ব্যক্তি দ্বারা একটি সংখ্যা ইনপুট করার মাধ্যমে আসতে পারে। যদি তারা ভুলবশত একটি খুব বড় সংখ্যা প্রবেশ করায় এবং প্রোগ্রামটি একটি `None` ভ্যালু পায়, আপনি ব্যবহারকারীকে বলতে পারেন বর্তমান vector-এ কতগুলি আইটেম আছে এবং তাদের একটি বৈধ ভ্যালু প্রবেশ করার আরেকটি সুযোগ দিতে পারেন। এটি একটি টাইপের ভুলের জন্য প্রোগ্রাম ক্র্যাশ করার চেয়ে বেশি ব্যবহারকারী-বান্ধব হবে!

যখন প্রোগ্রামের একটি বৈধ রেফারেন্স থাকে, তখন borrow checker মালিকানা এবং ধার করার নিয়মগুলি (অধ্যায় ৪-এ আচ্ছাদিত) প্রয়োগ করে যাতে এই রেফারেন্স এবং vector-এর বিষয়বস্তুর অন্য কোনো রেফারেন্স বৈধ থাকে। সেই নিয়মটি স্মরণ করুন যা বলে যে আপনি একই স্কোপে mutable এবং immutable রেফারেন্স রাখতে পারবেন না। সেই নিয়মটি লিস্টিং ৮-৬-এ প্রযোজ্য, যেখানে আমরা একটি vector-এর প্রথম element-এর একটি immutable রেফারেন্স ধরে রাখি এবং শেষে একটি element যোগ করার চেষ্টা করি। এই প্রোগ্রামটি কাজ করবে না যদি আমরা ফাংশনের পরে সেই element-কে আবার রেফারেন্স করার চেষ্টা করি।

<Listing number="8-6" caption="একটি আইটেমের রেফারেন্স ধরে রেখে একটি vector-এ একটি element যোগ করার চেষ্টা">

```rust,ignore,does_not_compile
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}

এই কোডটি কম্পাইল করলে এই error-টি আসবে:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                     ------- immutable borrow later used here

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

লিস্টিং ৮-৬ এর কোডটি দেখে মনে হতে পারে যে এটি কাজ করা উচিত: প্রথম element-এর একটি রেফারেন্স vector-এর শেষের পরিবর্তনে কেন পাত্তা দেবে? এই error-টি vector-এর কাজ করার পদ্ধতির কারণে হয়: কারণ vector মেমরিতে ভ্যালুগুলো একে অপরের পাশে রাখে, vector-এর শেষে একটি নতুন element যোগ করার জন্য নতুন মেমরি বরাদ্দ করার এবং পুরানো element-গুলো নতুন জায়গায় অনুলিপি করার প্রয়োজন হতে পারে, যদি vector-টি যেখানে বর্তমানে সংরক্ষিত আছে সেখানে সমস্ত element পাশাপাশি রাখার জন্য পর্যাপ্ত জায়গা না থাকে। সেক্ষেত্রে, প্রথম element-এর রেফারেন্সটি একটি ডিঅ্যালোকেটেড মেমরির দিকে নির্দেশ করবে। ধার করার নিয়মগুলি প্রোগ্রামগুলিকে সেই পরিস্থিতিতে পড়া থেকে বিরত রাখে।

দ্রষ্টব্য: Vec<T> টাইপের বাস্তবায়নের বিবরণ সম্পর্কে আরও জানতে, "The Rustonomicon" দেখুন।

Vector-এর ভ্যালুগুলোর উপর Iterate করা

একটি vector-এর প্রতিটি element-কে পর্যায়ক্রমে অ্যাক্সেস করার জন্য, আমরা একবারে একটি অ্যাক্সেস করার জন্য index ব্যবহার না করে সমস্ত element-এর মধ্যে দিয়ে iterate করব। লিস্টিং ৮-৭ দেখায় কীভাবে একটি for লুপ ব্যবহার করে i32 ভ্যালুর একটি vector-এর প্রতিটি element-এর immutable রেফারেন্স পেতে এবং সেগুলি প্রিন্ট করতে হয়।

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}

আমরা সমস্ত element-এ পরিবর্তন আনার জন্য একটি mutable vector-এর প্রতিটি element-এর mutable রেফারেন্সের উপরও iterate করতে পারি। লিস্টিং ৮-৮ এর for লুপ প্রতিটি element-এর সাথে 50 যোগ করবে।

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

mutable রেফারেন্স যে ভ্যালুকে নির্দেশ করে তা পরিবর্তন করতে, আমাদের * dereference operator ব্যবহার করতে হবে i-এর ভ্যালুটি পাওয়ার জন্য, তারপর আমরা += operator ব্যবহার করতে পারি। আমরা dereference operator সম্পর্কে অধ্যায় ১৫-এর "Following the Reference to the Value" বিভাগে আরও কথা বলব।

একটি vector-এর উপর iterate করা, immutable হোক বা mutable, borrow checker-এর নিয়মের কারণে নিরাপদ। যদি আমরা লিস্টিং ৮-৭ এবং লিস্টিং ৮-৮ এর for লুপের বডিতে আইটেম ঢোকানো বা সরানোর চেষ্টা করতাম, আমরা লিস্টিং ৮-৬ এর কোডের মতো একটি compiler error পেতাম। for লুপ যে vector-এর রেফারেন্স ধরে রাখে তা পুরো vector-এর একযোগে পরিবর্তন প্রতিরোধ করে।

একাধিক Type স্টোর করার জন্য Enum ব্যবহার

Vector শুধুমাত্র একই টাইপের ভ্যালু স্টোর করতে পারে। এটি অসুবিধাজনক হতে পারে; বিভিন্ন টাইপের আইটেমের তালিকা স্টোর করার প্রয়োজন অবশ্যই আছে। সৌভাগ্যবশত, একটি enum-এর ভ্যারিয়েন্টগুলি একই enum টাইপের অধীনে সংজ্ঞায়িত করা হয়, তাই যখন আমাদের বিভিন্ন টাইপের element প্রতিনিধিত্ব করার জন্য একটি টাইপের প্রয়োজন হয়, তখন আমরা একটি enum সংজ্ঞায়িত এবং ব্যবহার করতে পারি!

উদাহরণস্বরূপ, ধরুন আমরা একটি স্প্রেডশীটের একটি সারি থেকে ভ্যালু পেতে চাই যেখানে সারির কিছু কলামে ইন্টিজার, কিছু ফ্লোটিং-পয়েন্ট নম্বর এবং কিছু স্ট্রিং রয়েছে। আমরা একটি enum সংজ্ঞায়িত করতে পারি যার ভ্যারিয়েন্টগুলি বিভিন্ন ভ্যালু টাইপ ধারণ করবে, এবং সমস্ত enum ভ্যারিয়েন্ট একই টাইপের বলে বিবেচিত হবে: সেই enum-এর টাইপ। তারপর আমরা সেই enum ধারণ করার জন্য একটি vector তৈরি করতে পারি এবং এইভাবে, অবশেষে, বিভিন্ন টাইপ ধারণ করতে পারি। আমরা এটি লিস্টিং ৮-৯-এ প্রদর্শন করেছি।

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}

Rust-কে compile time-এ জানতে হবে vector-এ কোন টাইপগুলো থাকবে যাতে এটি জানতে পারে প্রতিটি element স্টোর করার জন্য heap-এ ঠিক কতটা মেমরি লাগবে। এই vector-এ কোন টাইপগুলো অনুমোদিত সে সম্পর্কেও আমাদের সুস্পষ্ট হতে হবে। যদি Rust একটি vector-কে যেকোনো টাইপ ধারণ করার অনুমতি দিত, তাহলে একটি বা একাধিক টাইপ vector-এর element-গুলোর উপর সঞ্চালিত অপারেশনগুলির সাথে error ঘটাতে পারত। একটি enum এবং একটি match এক্সপ্রেশন ব্যবহার করার অর্থ হল যে Rust compile time-এ নিশ্চিত করবে যে প্রতিটি সম্ভাব্য কেস পরিচালনা করা হয়েছে, যেমনটি অধ্যায় ৬-এ আলোচনা করা হয়েছে।

যদি আপনি না জানেন যে একটি প্রোগ্রাম রানটাইমে একটি vector-এ স্টোর করার জন্য কোন কোন টাইপ পাবে, তাহলে enum কৌশলটি কাজ করবে না। এর পরিবর্তে, আপনি একটি trait object ব্যবহার করতে পারেন, যা আমরা অধ্যায় ১৮-এ আলোচনা করব।

এখন যেহেতু আমরা vector ব্যবহারের কিছু সবচেয়ে সাধারণ উপায় নিয়ে আলোচনা করেছি, standard library দ্বারা Vec<T>-তে সংজ্ঞায়িত সমস্ত দরকারি মেথডগুলির জন্য the API documentation পর্যালোচনা করতে ভুলবেন না। উদাহরণস্বরূপ, push ছাড়াও, একটি pop মেথড রয়েছে যা শেষ element-টি সরিয়ে দেয় এবং ফেরত দেয়।

Vector Drop হলে তার Element-গুলোও Drop হয়

অন্য যেকোনো struct-এর মতো, একটি vector যখন স্কোপের বাইরে চলে যায় তখন তা মুক্ত হয়ে যায়, যেমনটি লিস্টিং ৮-১০-এ দেখানো হয়েছে।

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}

যখন vector-টি ড্রপ করা হয়, তখন তার সমস্ত বিষয়বস্তুও ড্রপ করা হয়, যার মানে হল এটি যে ইন্টিজারগুলি ধারণ করে সেগুলি পরিষ্কার করা হবে। borrow checker নিশ্চিত করে যে একটি vector-এর বিষয়বস্তুর যেকোনো রেফারেন্স শুধুমাত্র তখনই ব্যবহৃত হয় যখন vector নিজেই বৈধ থাকে।

চলুন পরবর্তী collection টাইপ-এ যাওয়া যাক: String!