ভেক্টর ব্যবহার করে মানগুলোর তালিকা সংরক্ষণ করা (Storing Lists of Values with Vectors)
আমরা প্রথমে যে কালেকশন টাইপটি দেখব তা হল Vec<T>
, যাকে ভেক্টর (vector) বলা হয়। ভেক্টর আপনাকে মেমরিতে একে অপরের পাশে সমস্ত মান স্থাপন করে একটি একক ডেটা স্ট্রাকচারে একাধিক মান সংরক্ষণ করতে দেয়। ভেক্টরগুলো শুধুমাত্র একই টাইপের মান সংরক্ষণ করতে পারে। যখন আপনার কাছে আইটেমগুলোর একটি তালিকা থাকে, যেমন একটি ফাইলের টেক্সটের লাইন বা শপিং কার্টের আইটেমগুলোর দাম, তখন এগুলো দরকারী।
একটি নতুন ভেক্টর তৈরি করা (Creating a New Vector)
একটি নতুন খালি ভেক্টর তৈরি করতে, আমরা Vec::new
ফাংশনটি কল করি, যেমনটি Listing 8-1-এ দেখানো হয়েছে।
লক্ষ্য করুন যে আমরা এখানে একটি টাইপ অ্যানোটেশন যুক্ত করেছি। যেহেতু আমরা এই ভেক্টরে কোনো মান সন্নিবেশ করাচ্ছি না, তাই Rust জানে না যে আমরা কোন ধরনের উপাদান সংরক্ষণ করতে চাই। এটি একটি গুরুত্বপূর্ণ বিষয়। ভেক্টরগুলো জেনেরিক ব্যবহার করে ইমপ্লিমেন্ট করা হয়; আমরা চ্যাপ্টার ১০-এ আপনার নিজের টাইপের সাথে জেনেরিক কীভাবে ব্যবহার করতে হয় তা কভার করব। আপাতত, জেনে রাখুন যে স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা Vec<T>
টাইপ যেকোনো টাইপ ধারণ করতে পারে। যখন আমরা একটি নির্দিষ্ট টাইপ ধারণ করার জন্য একটি ভেক্টর তৈরি করি, তখন আমরা অ্যাঙ্গেল ব্র্যাকেটের মধ্যে টাইপটি নির্দিষ্ট করতে পারি। Listing 8-1-এ, আমরা Rust-কে বলেছি যে v
-এর Vec<T>
i32
টাইপের উপাদান ধারণ করবে।
প্রায়শই, আপনি প্রাথমিক মান সহ একটি Vec<T>
তৈরি করবেন এবং Rust আপনি যে মান সংরক্ষণ করতে চান তার টাইপ অনুমান করবে, তাই আপনাকে খুব কমই এই টাইপ অ্যানোটেশন করতে হবে। Rust সুবিধাজনকভাবে vec!
ম্যাক্রো সরবরাহ করে, যা আপনার দেওয়া মানগুলো ধারণ করে এমন একটি নতুন ভেক্টর তৈরি করবে। Listing 8-2 1
, 2
এবং 3
মান ধারণ করে এমন একটি নতুন Vec<i32>
তৈরি করে। ইন্টিজার টাইপ হল i32
কারণ এটি ডিফল্ট ইন্টিজার টাইপ, যেমনটি আমরা চ্যাপ্টার ৩-এর “ডেটা টাইপস” বিভাগে আলোচনা করেছি।
যেহেতু আমরা প্রাথমিক i32
মান দিয়েছি, তাই Rust অনুমান করতে পারে যে v
-এর টাইপ হল Vec<i32>
, এবং টাইপ অ্যানোটেশনটি প্রয়োজনীয় নয়। এরপর, আমরা দেখব কিভাবে একটি ভেক্টর পরিবর্তন করতে হয়।
একটি ভেক্টর আপডেট করা (Updating a Vector)
একটি ভেক্টর তৈরি করতে এবং তারপর এতে উপাদান যোগ করতে, আমরা push
মেথড ব্যবহার করতে পারি, যেমনটি Listing 8-3-তে দেখানো হয়েছে।
যেকোনো ভেরিয়েবলের মতো, যদি আমরা এর মান পরিবর্তন করতে চাই, তাহলে আমাদের mut
কীওয়ার্ড ব্যবহার করে এটিকে মিউটেবল করতে হবে, যেমনটি চ্যাপ্টার ৩-এ আলোচনা করা হয়েছে। আমরা ভিতরে যে সংখ্যাগুলো রাখি সেগুলো সবই i32
টাইপের, এবং Rust ডেটা থেকে এটি অনুমান করে, তাই আমাদের Vec<i32>
অ্যানোটেশনের প্রয়োজন নেই।
ভেক্টরের উপাদানগুলো পড়া (Reading Elements of Vectors)
একটি ভেক্টরে সংরক্ষিত একটি মান রেফারেন্স করার দুটি উপায় রয়েছে: ইনডেক্সিংয়ের মাধ্যমে অথবা get
মেথড ব্যবহার করে। নিম্নলিখিত উদাহরণগুলোতে, আমরা অতিরিক্ত স্পষ্টতার জন্য এই ফাংশনগুলো থেকে রিটার্ন করা মানগুলোর টাইপ অ্যানোটেট করেছি।
Listing 8-4 একটি ভেক্টরের একটি মান অ্যাক্সেস করার উভয় পদ্ধতি দেখায়, ইনডেক্সিং সিনট্যাক্স এবং get
মেথড দিয়ে।
এখানে কয়েকটি বিবরণ লক্ষ্য করুন। আমরা তৃতীয় উপাদানটি পেতে 2
-এর ইনডেক্স মান ব্যবহার করি কারণ ভেক্টরগুলো সংখ্যা দ্বারা ইনডেক্স করা হয়, শূন্য থেকে শুরু করে। &
এবং []
ব্যবহার করা আমাদের ইনডেক্স মানের এলিমেন্টের একটি রেফারেন্স দেয়। যখন আমরা আর্গুমেন্ট হিসাবে পাস করা ইনডেক্স সহ get
মেথড ব্যবহার করি, তখন আমরা একটি Option<&T>
পাই যা আমরা match
-এর সাথে ব্যবহার করতে পারি।
Rust এলিমেন্টকে রেফারেন্স করার এই দুটি উপায় সরবরাহ করে যাতে আপনি প্রোগ্রামটি কীভাবে আচরণ করবে তা বেছে নিতে পারেন যখন আপনি বিদ্যমান এলিমেন্টগুলোর সীমার বাইরের একটি ইনডেক্স মান ব্যবহার করার চেষ্টা করেন। উদাহরণস্বরূপ, আসুন দেখি কী ঘটে যখন আমাদের পাঁচটি এলিমেন্টসহ একটি ভেক্টর থাকে এবং তারপর আমরা প্রতিটি কৌশল দিয়ে ১০০ ইনডেক্সে একটি এলিমেন্ট অ্যাক্সেস করার চেষ্টা করি, যেমনটি Listing 8-5-এ দেখানো হয়েছে।
যখন আমরা এই কোডটি চালাই, তখন প্রথম []
মেথডটি প্রোগ্রামটিকে প্যানিক (panic) ঘটাবে কারণ এটি একটি অস্তিত্বহীন এলিমেন্টকে রেফারেন্স করে। এই মেথডটি তখনই ব্যবহার করা ভাল যখন আপনি চান যে আপনার প্রোগ্রামটি ক্র্যাশ করুক যদি ভেক্টরের শেষের বাইরের কোনো এলিমেন্ট অ্যাক্সেস করার চেষ্টা করা হয়।
যখন get
মেথডটিকে ভেক্টরের বাইরের একটি ইনডেক্স পাস করা হয়, তখন এটি প্যানিক না করে None
রিটার্ন করে। আপনি এই মেথডটি ব্যবহার করবেন যদি ভেক্টরের সীমার বাইরের একটি এলিমেন্ট অ্যাক্সেস করা স্বাভাবিক পরিস্থিতিতে মাঝে মাঝে ঘটতে পারে। আপনার কোডে তখন Some(&element)
বা None
হ্যান্ডেল করার জন্য লজিক থাকবে, যেমনটি চ্যাপ্টার ৬-এ আলোচনা করা হয়েছে। উদাহরণস্বরূপ, ইনডেক্সটি একজন ব্যক্তির কাছ থেকে একটি সংখ্যা প্রবেশ করানো থেকে আসতে পারে। যদি তারা ভুলবশত খুব বড় একটি সংখ্যা প্রবেশ করায় এবং প্রোগ্রামটি একটি None
মান পায়, তাহলে আপনি ব্যবহারকারীকে বলতে পারেন যে বর্তমান ভেক্টরে কতগুলো আইটেম রয়েছে এবং তাদের একটি বৈধ মান প্রবেশ করার আরেকটি সুযোগ দিতে পারেন। একটি টাইপোর কারণে প্রোগ্রামটি ক্র্যাশ করার চেয়ে এটি আরও ব্যবহারকারী-বান্ধব হবে!
যখন প্রোগ্রামের একটি বৈধ রেফারেন্স থাকে, তখন বোরো চেকার (borrow checker) ওনারশিপ এবং বোরোয়িং-এর নিয়মগুলো (চ্যাপ্টার ৪-এ কভার করা হয়েছে) প্রয়োগ করে যাতে এই রেফারেন্স এবং ভেক্টরের কনটেন্টের অন্য কোনো রেফারেন্স বৈধ থাকে। সেই নিয়মটি স্মরণ করুন যা বলে যে আপনি একই স্কোপে মিউটেবল এবং ইমিউটেবল রেফারেন্স রাখতে পারবেন না। সেই নিয়মটি Listing 8-6-এ প্রযোজ্য, যেখানে আমরা ভেক্টরের প্রথম এলিমেন্টের একটি ইমিউটেবল রেফারেন্স রাখি এবং শেষে একটি এলিমেন্ট যোগ করার চেষ্টা করি। যদি আমরা ফাংশনের পরেও সেই এলিমেন্টটিকে রেফার করার চেষ্টা করি তবে এই প্রোগ্রামটি কাজ করবে না।
এই কোডটি কম্পাইল করলে এই এররটি আসবে:
$ 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
Listing 8-6-এর কোডটি কাজ করা উচিত বলে মনে হতে পারে: প্রথম এলিমেন্টের একটি রেফারেন্স কেন ভেক্টরের শেষের পরিবর্তনগুলোর বিষয়ে চিন্তা করবে? এই এররটি ভেক্টরগুলো যেভাবে কাজ করে তার কারণে: যেহেতু ভেক্টরগুলো মেমরিতে মানগুলোকে একে অপরের পাশে রাখে, তাই ভেক্টরের শেষে একটি নতুন এলিমেন্ট যোগ করার জন্য নতুন মেমরি বরাদ্দ করার এবং পুরানো এলিমেন্টগুলোকে নতুন জায়গায় কপি করার প্রয়োজন হতে পারে, যদি ভেক্টরটি বর্তমানে যেখানে সংরক্ষণ করা হয়েছে সেখানে সমস্ত এলিমেন্টগুলোকে একে অপরের পাশে রাখার জন্য যথেষ্ট জায়গা না থাকে। সেই ক্ষেত্রে, প্রথম এলিমেন্টের রেফারেন্সটি ডিলোক্যাট করা মেমরির দিকে নির্দেশ করবে। বোরোয়িং-এর নিয়মগুলো প্রোগ্রামগুলোকে সেই পরিস্থিতিতে পড়তে বাধা দেয়।
দ্রষ্টব্য:
Vec<T>
টাইপের ইমপ্লিমেন্টেশনের বিস্তারিত বিবরণের জন্য, “The Rustonomicon” দেখুন।
একটি ভেক্টরের মানগুলোর উপর ইটারেট করা (Iterating Over the Values in a Vector)
একটি ভেক্টরের প্রতিটি এলিমেন্ট অ্যাক্সেস করার জন্য, আমরা একবারে একটি অ্যাক্সেস করার জন্য ইনডেক্স ব্যবহার করার পরিবর্তে সমস্ত এলিমেন্টের মধ্য দিয়ে ইটারেট করব। Listing 8-7 দেখায় কিভাবে i32
মানগুলোর একটি ভেক্টরের প্রতিটি এলিমেন্টে ইমিউটেবল রেফারেন্স পেতে এবং সেগুলো প্রিন্ট করতে একটি for
লুপ ব্যবহার করতে হয়।
সমস্ত এলিমেন্টে পরিবর্তন করার জন্য আমরা একটি মিউটেবল ভেক্টরের প্রতিটি এলিমেন্টে মিউটেবল রেফারেন্সের উপরও ইটারেট করতে পারি। Listing 8-8-এর for
লুপ প্রতিটি এলিমেন্টে 50
যোগ করবে।
মিউটেবল রেফারেন্স যে মানটিকে রেফার করে সেটি পরিবর্তন করতে, +=
অপারেটর ব্যবহার করার আগে আমাদের i
-এর মান পেতে *
ডিরেফারেন্স অপারেটর ব্যবহার করতে হবে। আমরা চ্যাপ্টার ১৫-এর “ভ্যালুতে পয়েন্টার অনুসরণ করা” বিভাগে ডিরেফারেন্স অপারেটর সম্পর্কে আরও কথা বলব।
একটি ভেক্টরের উপর ইটারেট করা, ইমিউটেবল বা মিউটেবল যাই হোক না কেন, বোরো চেকারের নিয়মের কারণে নিরাপদ। যদি আমরা Listing 8-7 এবং Listing 8-8-এর for
লুপ বডিতে আইটেমগুলো সন্নিবেশ বা অপসারণ করার চেষ্টা করতাম, তাহলে আমরা Listing 8-6-এর কোডের সাথে পাওয়া এররের মতো একটি কম্পাইলার এরর পেতাম। for
লুপ যে ভেক্টরটির রেফারেন্স ধরে রাখে সেটি সম্পূর্ণ ভেক্টরের যুগপৎ পরিবর্তন প্রতিরোধ করে।
একাধিক টাইপ সংরক্ষণ করতে একটি এনাম ব্যবহার করা (Using an Enum to Store Multiple Types)
ভেক্টরগুলো শুধুমাত্র একই টাইপের মান সংরক্ষণ করতে পারে। এটি অসুবিধাজনক হতে পারে; বিভিন্ন টাইপের আইটেমগুলোর একটি তালিকা সংরক্ষণ করার প্রয়োজনের জন্য অবশ্যই ব্যবহারের ক্ষেত্র রয়েছে। সৌভাগ্যবশত, একটি এনামের ভেরিয়েন্টগুলো একই এনাম টাইপের অধীনে সংজ্ঞায়িত করা হয়, তাই যখন আমাদের বিভিন্ন টাইপের এলিমেন্ট উপস্থাপন করার জন্য একটি টাইপ প্রয়োজন হয়, তখন আমরা একটি এনাম সংজ্ঞায়িত এবং ব্যবহার করতে পারি!
উদাহরণস্বরূপ, ধরা যাক আমরা একটি স্প্রেডশিটের একটি সারি থেকে মান পেতে চাই যেখানে সারির কিছু কলামে ইন্টিজার, কিছুতে ফ্লোটিং-পয়েন্ট সংখ্যা এবং কিছুতে স্ট্রিং রয়েছে। আমরা একটি এনাম সংজ্ঞায়িত করতে পারি যার ভেরিয়েন্টগুলো বিভিন্ন মানের টাইপ ধারণ করবে এবং সমস্ত এনাম ভেরিয়েন্ট একই টাইপ হিসাবে বিবেচিত হবে: এনামের টাইপ। তারপর আমরা সেই এনামটি ধারণ করার জন্য একটি ভেক্টর তৈরি করতে পারি এবং শেষ পর্যন্ত, বিভিন্ন টাইপ ধারণ করতে পারি। আমরা Listing 8-9-এ এটি প্রদর্শন করেছি।
Rust-কে কম্পাইল করার সময় জানতে হবে ভেক্টরে কোন টাইপগুলো থাকবে যাতে এটি জানে যে প্রতিটি এলিমেন্ট সংরক্ষণ করার জন্য হিপে ঠিক কতটা মেমরির প্রয়োজন হবে। আমাদের অবশ্যই স্পষ্ট হতে হবে যে এই ভেক্টরে কোন টাইপগুলো অনুমোদিত। যদি Rust একটি ভেক্টরকে যেকোনো টাইপ ধারণ করার অনুমতি দিত, তাহলে ভেক্টরের এলিমেন্টগুলোতে করা অপারেশনের সাথে এক বা একাধিক টাইপ এরর ঘটাতে পারত। একটি এনাম প্লাস একটি match
এক্সপ্রেশন ব্যবহার করার অর্থ হল Rust কম্পাইল করার সময় নিশ্চিত করবে যে প্রতিটি সম্ভাব্য ক্ষেত্র হ্যান্ডেল করা হয়েছে, যেমনটি চ্যাপ্টার ৬-এ আলোচনা করা হয়েছে।
আপনি যদি জানেন না যে একটি প্রোগ্রাম রানটাইমে একটি ভেক্টরে সংরক্ষণ করার জন্য কোন টাইপের সম্পূর্ণ সেট পাবে, তাহলে এনাম কৌশলটি কাজ করবে না। পরিবর্তে, আপনি একটি ট্রেইট অবজেক্ট ব্যবহার করতে পারেন, যা আমরা চ্যাপ্টার 18-এ কভার করব।
এখন আমরা ভেক্টর ব্যবহার করার সবচেয়ে সাধারণ উপায়গুলো নিয়ে আলোচনা করেছি, API ডকুমেন্টেশন পর্যালোচনা করতে ভুলবেন না, স্ট্যান্ডার্ড লাইব্রেরি দ্বারা Vec<T>
-তে সংজ্ঞায়িত আরও অনেক দরকারী মেথডের জন্য। উদাহরণস্বরূপ, push
ছাড়াও, একটি pop
মেথড শেষ এলিমেন্টটিকে সরিয়ে দেয় এবং রিটার্ন করে।
একটি ভেক্টর ড্রপ করলে এর এলিমেন্টগুলো ড্রপ হয় (Dropping a Vector Drops Its Elements)
অন্য যেকোনো struct
-এর মতো, একটি ভেক্টর যখন স্কোপের বাইরে চলে যায় তখন মুক্ত হয়ে যায়, যেমনটি Listing 8-10-এ টীকা দেওয়া হয়েছে।
যখন ভেক্টরটি ড্রপ করা হয়, তখন এর সমস্ত কনটেন্টও ড্রপ করা হয়, যার অর্থ এটি যে ইন্টিজারগুলো ধারণ করে সেগুলো পরিষ্কার করা হবে। বোরো চেকার নিশ্চিত করে যে একটি ভেক্টরের কনটেন্টের যেকোনো রেফারেন্স শুধুমাত্র তখনই ব্যবহার করা হয় যখন ভেক্টরটি নিজেই বৈধ থাকে।
আসুন পরবর্তী কালেকশন টাইপে যাওয়া যাক: String
!