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

স্ট্রিং ব্যবহার করে UTF-8 এনকোডেড টেক্সট স্টোর করা

আমরা অধ্যায় ৪-এ স্ট্রিং নিয়ে আলোচনা করেছি, কিন্তু এখন আমরা এটি আরও গভীরভাবে দেখব। নতুন Rust ব্যবহারকারীরা সাধারণত তিনটি কারণে স্ট্রিং নিয়ে সমস্যায় পড়েন: সম্ভাব্য error তুলে ধরার ক্ষেত্রে Rust-এর প্রবণতা, স্ট্রিং ডেটা স্ট্রাকচারটি যতটা সহজ ভাবা হয় তার চেয়ে বেশি জটিল হওয়া, এবং UTF-8। এই কারণগুলো একত্রিত হয়ে এমন পরিস্থিতি তৈরি করে যা অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজ থেকে আসা ডেভেলপারদের জন্য কঠিন মনে হতে পারে।

আমরা collections-এর প্রেক্ষাপটে স্ট্রিং নিয়ে আলোচনা করছি কারণ স্ট্রিং মূলত বাইটের (bytes) একটি collection হিসাবে প্রয়োগ করা হয়, এবং এর সাথে কিছু অতিরিক্ত মেথড থাকে যা সেই বাইটগুলোকে টেক্সট হিসাবে ব্যাখ্যা করার সময় দরকারি কার্যকারিতা প্রদান করে। এই বিভাগে, আমরা String-এর সেইসব অপারেশন নিয়ে কথা বলব যা প্রতিটি collection টাইপের মধ্যেই রয়েছে, যেমন তৈরি করা, আপডেট করা এবং পড়া। এছাড়াও আমরা আলোচনা করব String অন্যান্য collection থেকে কোন কোন ক্ষেত্রে আলাদা, বিশেষ করে মানুষ এবং কম্পিউটার যেভাবে String ডেটাকে ব্যাখ্যা করে তার পার্থক্যের কারণে String-এ ইনডেক্সিং (indexing) করাটা বেশ জটিল।

স্ট্রিং কী?

প্রথমে আমরা স্ট্রিং বলতে কী বুঝি তা নির্ধারণ করব। Rust-এর কোর ল্যাঙ্গুয়েজে শুধুমাত্র একটি স্ট্রিং টাইপ আছে, যা হলো স্ট্রিং স্লাইস str, এবং এটি সাধারণত এর ধার করা (borrowed) রূপ &str-এ দেখা যায়। অধ্যায় ৪-এ, আমরা স্ট্রিং স্লাইস নিয়ে কথা বলেছিলাম, যা অন্য কোথাও স্টোর করা UTF-8 এনকোডেড স্ট্রিং ডেটার রেফারেন্স। উদাহরণস্বরূপ, স্ট্রিং লিটারেল (string literals) প্রোগ্রামের বাইনারিতে স্টোর করা হয় এবং তাই সেগুলি স্ট্রিং স্লাইস।

String টাইপটি Rust-এর standard library দ্বারা সরবরাহ করা হয়, এটি কোর ল্যাঙ্গুয়েজে কোড করা নেই। এটি একটি পরিবর্তনশীল (growable), পরিবর্তনযোগ্য (mutable), নিজস্ব (owned), এবং UTF-8 এনকোডেড স্ট্রিং টাইপ। যখন Rust ব্যবহারকারীরা "স্ট্রিং" বলেন, তখন তারা String বা স্ট্রিং স্লাইস &str উভয়কেই বোঝাতে পারেন, শুধু একটিকে নয়। যদিও এই বিভাগটি মূলত String সম্পর্কিত, উভয় টাইপই Rust-এর standard library-তে ব্যাপকভাবে ব্যবহৃত হয় এবং String ও স্ট্রিং স্লাইস উভয়ই UTF-8 এনকোডেড।

নতুন স্ট্রিং তৈরি করা

Vec<T>-এর সাথে উপলব্ধ অনেক অপারেশন String-এর সাথেও উপলব্ধ, কারণ String আসলে বাইটের একটি vector-এর উপর একটি র‍্যাপার (wrapper) হিসাবে প্রয়োগ করা হয়েছে, যাতে কিছু অতিরিক্ত গ্যারান্টি, সীমাবদ্ধতা এবং ক্ষমতা রয়েছে। Vec<T> এবং String-এর সাথে একইভাবে কাজ করে এমন একটি ফাংশনের উদাহরণ হলো new ফাংশন, যা একটি ইনস্ট্যান্স তৈরি করতে ব্যবহৃত হয়, যেমনটি লিস্টিং ৮-১১-তে দেখানো হয়েছে।

fn main() {
    let mut s = String::new();
}

এই লাইনটি s নামে একটি নতুন, খালি স্ট্রিং তৈরি করে, যেখানে আমরা পরে ডেটা লোড করতে পারব। প্রায়শই, আমাদের কাছে কিছু প্রাথমিক ডেটা থাকে যা দিয়ে আমরা স্ট্রিং শুরু করতে চাই। এর জন্য, আমরা to_string মেথড ব্যবহার করি, যা Display trait প্রয়োগকারী যেকোনো টাইপের উপর উপলব্ধ, যেমন স্ট্রিং লিটারেল। লিস্টিং ৮-১২ দুটি উদাহরণ দেখায়।

fn main() {
    let data = "initial contents";

    let s = data.to_string();

    // The method also works on a literal directly:
    let s = "initial contents".to_string();
}

এই কোডটি initial contents লেখা সহ একটি স্ট্রিং তৈরি করে।

আমরা String::from ফাংশন ব্যবহার করেও একটি স্ট্রিং লিটারেল থেকে String তৈরি করতে পারি। লিস্টিং ৮-১৩-এর কোডটি লিস্টিং ৮-১২-এর কোডের সমতুল্য যা to_string ব্যবহার করে।

fn main() {
    let s = String::from("initial contents");
}

যেহেতু স্ট্রিং অনেক কিছুর জন্য ব্যবহৃত হয়, আমরা স্ট্রিং-এর জন্য বিভিন্ন জেনেরিক API ব্যবহার করতে পারি, যা আমাদের অনেক বিকল্প সরবরাহ করে। তাদের মধ্যে কিছু অপ্রয়োজনীয় মনে হতে পারে, কিন্তু সবগুলোরই নিজস্ব স্থান আছে! এক্ষেত্রে, String::from এবং to_string একই কাজ করে, তাই আপনি কোনটি বেছে নেবেন তা আপনার স্টাইল এবং পঠনযোগ্যতার উপর নির্ভর করে।

মনে রাখবেন যে স্ট্রিংগুলো UTF-8 এনকোডেড, তাই আমরা সেগুলিতে যেকোনো সঠিকভাবে এনকোড করা ডেটা অন্তর্ভুক্ত করতে পারি, যেমনটি লিস্টিং ৮-১৪-তে দেখানো হয়েছে।

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שלום");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

এগুলো সবই বৈধ String ভ্যালু।

একটি স্ট্রিং আপডেট করা

একটি String-এর আকার বাড়তে পারে এবং এর বিষয়বস্তু পরিবর্তন হতে পারে, যেমন Vec<T>-এর বিষয়বস্তু পরিবর্তন করা যায়, যদি আপনি এতে আরও ডেটা পুশ করেন। এছাড়াও, আপনি সুবিধামত + অপারেটর বা format! ম্যাক্রো ব্যবহার করে String ভ্যালু সংযুক্ত (concatenate) করতে পারেন।

push_str এবং push দিয়ে একটি স্ট্রিং-এ যোগ করা

আমরা push_str মেথড ব্যবহার করে একটি স্ট্রিং স্লাইস যোগ করে একটি String বড় করতে পারি, যেমনটি লিস্টিং ৮-১৫-তে দেখানো হয়েছে।

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");
}

এই দুটি লাইনের পরে, s-এ foobar থাকবে। push_str মেথডটি একটি স্ট্রিং স্লাইস নেয় কারণ আমরা প্যারামিটারের মালিকানা (ownership) নিতে চাই না। উদাহরণস্বরূপ, লিস্টিং ৮-১৬-এর কোডে, আমরা s1-এ s2-এর বিষয়বস্তু যোগ করার পরেও s2 ব্যবহার করতে চাই।

fn main() {
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {s2}");
}

যদি push_str মেথডটি s2-এর মালিকানা নিয়ে নিত, আমরা শেষ লাইনে এর ভ্যালু প্রিন্ট করতে পারতাম না। কিন্তু এই কোডটি আমাদের প্রত্যাশা অনুযায়ী কাজ করে!

push মেথডটি একটি একক ক্যারেক্টার (character) প্যারামিটার হিসাবে নেয় এবং এটি String-এ যোগ করে। লিস্টিং ৮-১৭ push মেথড ব্যবহার করে একটি String-এ l অক্ষরটি যোগ করে।

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}

এর ফলে, s-এ lol থাকবে।

+ অপারেটর বা format! ম্যাক্রো দিয়ে Concatenation

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

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}

s3 স্ট্রিংটিতে Hello, world! থাকবে। s1 যোগ করার পরে আর বৈধ থাকে না এবং আমরা s2-এর একটি রেফারেন্স ব্যবহার করেছি, এর কারণটি সেই মেথডের সিগনেচারের সাথে সম্পর্কিত যা + অপারেটর ব্যবহার করার সময় কল করা হয়। + অপারেটরটি add মেথড ব্যবহার করে, যার সিগনেচারটি প্রায় এরকম:

fn add(self, s: &str) -> String {

Standard library-তে, আপনি add মেথডটি জেনেরিক এবং অ্যাসোসিয়েটেড টাইপ ব্যবহার করে সংজ্ঞায়িত দেখতে পাবেন। এখানে, আমরা সুনির্দিষ্ট টাইপ ব্যবহার করেছি, যা ঘটে যখন আমরা String ভ্যালু দিয়ে এই মেথডটি কল করি। আমরা অধ্যায় ১০-এ জেনেরিক নিয়ে আলোচনা করব। এই সিগনেচারটি আমাদের + অপারেটরের জটিল অংশগুলো বোঝার জন্য প্রয়োজনীয় সূত্র দেয়।

প্রথমত, s2-এর একটি & আছে, যার মানে আমরা প্রথম স্ট্রিং-এর সাথে দ্বিতীয় স্ট্রিং-এর একটি রেফারেন্স যোগ করছি। এটি add ফাংশনের s প্যারামিটারের কারণে: আমরা শুধুমাত্র একটি &str একটি String-এ যোগ করতে পারি; আমরা দুটি String ভ্যালু একসাথে যোগ করতে পারি না। কিন্তু অপেক্ষা করুন—&s2-এর টাইপ হলো &String, &str নয়, যেমনটি add-এর দ্বিতীয় প্যারামিটারে নির্দিষ্ট করা আছে। তাহলে লিস্টিং ৮-১৮ কেন কম্পাইল হয়?

add কলে &s2 ব্যবহার করতে পারার কারণ হলো কম্পাইলার &String আর্গুমেন্টটিকে একটি &str-এ coerce (রূপান্তর) করতে পারে। যখন আমরা add মেথডটি কল করি, Rust একটি deref coercion ব্যবহার করে, যা এখানে &s2-কে &s2[..]-তে পরিণত করে। আমরা অধ্যায় ১৫-এ deref coercion নিয়ে আরও বিস্তারিত আলোচনা করব। যেহেতু add s প্যারামিটারের মালিকানা নেয় না, তাই এই অপারেশনের পরেও s2 একটি বৈধ String থাকবে।

দ্বিতীয়ত, আমরা সিগনেচারে দেখতে পাচ্ছি যে add self-এর মালিকানা নেয় কারণ self-এর আগে & নেই। এর মানে লিস্টিং ৮-১৮-এর s1 add কলের মধ্যে মুভ (move) হয়ে যাবে এবং তারপরে আর বৈধ থাকবে না। সুতরাং, যদিও let s3 = s1 + &s2; দেখে মনে হচ্ছে এটি উভয় স্ট্রিং কপি করে একটি নতুন তৈরি করবে, এই স্টেটমেন্টটি আসলে s1-এর মালিকানা নেয়, s2-এর বিষয়বস্তুর একটি কপি যোগ করে এবং তারপর ফলাফলের মালিকানা ফেরত দেয়। অন্য কথায়, এটি দেখতে অনেক কপি করার মতো মনে হলেও, এর বাস্তবায়ন কপি করার চেয়ে অনেক বেশি কার্যকর।

যদি আমাদের একাধিক স্ট্রিং যুক্ত করতে হয়, তাহলে + অপারেটরের ব্যবহার বেশ громоздким (unwieldy) হয়ে যায়:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
}

এই মুহুর্তে, s হবে tic-tac-toe। এতগুলো + এবং " অক্ষরের কারণে কী ঘটছে তা বোঝা কঠিন। আরও জটিল উপায়ে স্ট্রিং একত্রিত করার জন্য, আমরা এর পরিবর্তে format! ম্যাক্রো ব্যবহার করতে পারি:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{s1}-{s2}-{s3}");
}

এই কোডটি s-কে tic-tac-toe-তে সেট করে। format! ম্যাক্রো println!-এর মতো কাজ করে, কিন্তু আউটপুট স্ক্রিনে প্রিন্ট করার পরিবর্তে, এটি বিষয়বস্তুসহ একটি String ফেরত দেয়। format! ব্যবহার করা কোডের সংস্করণটি পড়া অনেক সহজ, এবং format! ম্যাক্রো দ্বারা তৈরি কোড রেফারেন্স ব্যবহার করে যাতে এই কলটি তার কোনো প্যারামিটারের মালিকানা না নেয়।

স্ট্রিং-এ ইনডেক্সিং

অন্যান্য অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজে, ইনডেক্স দ্বারা একটি স্ট্রিং-এর স্বতন্ত্র অক্ষর অ্যাক্সেস করা একটি বৈধ এবং সাধারণ অপারেশন। যাইহোক, আপনি যদি Rust-এ ইনডেক্সিং সিনট্যাক্স ব্যবহার করে একটি String-এর অংশ অ্যাক্সেস করার চেষ্টা করেন, আপনি একটি এরর পাবেন। লিস্টিং ৮-১৯-এর অবৈধ কোডটি বিবেচনা করুন।

fn main() {
    let s1 = String::from("hi");
    let h = s1[0];
}

এই কোডটি নিম্নলিখিত এরর দেবে:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> src/main.rs:3:16
  |
3 |     let h = s1[0];
  |                ^ string indices are ranges of `usize`
  |
  = note: you can use `.chars().nth()` or `.bytes().nth()`
          for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
  = help: the trait `SliceIndex<str>` is not implemented for `{integer}`
          but trait `SliceIndex<[_]>` is implemented for `usize`
  = help: for that trait implementation, expected `[_]`, found `str`
  = note: required for `String` to implement `Index<{integer}>`

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

এরর এবং নোটটি পুরো গল্পটি বলে দেয়: Rust স্ট্রিং ইনডেক্সিং সমর্থন করে না। কিন্তু কেন নয়? এই প্রশ্নের উত্তর দিতে, আমাদের আলোচনা করতে হবে Rust কীভাবে মেমরিতে স্ট্রিং স্টোর করে।

অভ্যন্তরীণ উপস্থাপনা (Internal Representation)

একটি String হলো Vec<u8>-এর উপর একটি র‍্যাপার। আসুন লিস্টিং ৮-১৪ থেকে আমাদের সঠিকভাবে এনকোড করা UTF-8 উদাহরণ স্ট্রিংগুলির কিছু দেখি। প্রথমত, এটি:

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שלום");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

এই ক্ষেত্রে, len হবে 4, যার মানে "Hola" স্ট্রিংটি স্টোর করা ভেক্টরটি ৪ বাইট দীর্ঘ। UTF-8-এ এনকোড করার সময় এই অক্ষরগুলির প্রতিটি এক বাইট করে জায়গা নেয়। তবে, নিম্নলিখিত লাইনটি আপনাকে অবাক করতে পারে (লক্ষ্য করুন যে এই স্ট্রিংটি ক্যাপিটাল সিরিলিক অক্ষর Ze দিয়ে শুরু হয়, সংখ্যা 3 দিয়ে নয়):

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שלום");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

আপনাকে যদি জিজ্ঞাসা করা হয় স্ট্রিংটি কত দীর্ঘ, আপনি হয়তো বলবেন ১২। আসলে, Rust-এর উত্তর হলো ২৪: এটি "Здравствуйте" শব্দটি UTF-8-এ এনকোড করতে প্রয়োজনীয় বাইটের সংখ্যা, কারণ সেই স্ট্রিং-এর প্রতিটি ইউনিকোড স্কেলার ভ্যালু ২ বাইট স্টোরেজ নেয়। অতএব, স্ট্রিং-এর বাইটগুলিতে একটি ইনডেক্স সবসময় একটি বৈধ ইউনিকোড স্কেলার ভ্যালুর সাথে মিলবে না। এটি দেখানোর জন্য, এই অবৈধ Rust কোডটি বিবেচনা করুন:

let hello = "Здравствуйте";
let answer = &hello[0];

আপনি ಈಗಾಗಲೇ জানেন যে answer З হবে না, যা প্রথম অক্ষর। UTF-8-এ এনকোড করা হলে, З-এর প্রথম বাইট হলো 208 এবং দ্বিতীয়টি হলো 151, তাই মনে হতে পারে যে answer আসলে 208 হওয়া উচিত, কিন্তু 208 নিজে থেকে একটি বৈধ অক্ষর নয়। যদি কেউ এই স্ট্রিংয়ের প্রথম অক্ষরের জন্য জিজ্ঞাসা করে তবে 208 ফেরত দেওয়া সম্ভবত ব্যবহারকারীর কাঙ্ক্ষিত হবে না; তবে, বাইট ইনডেক্স ০-তে Rust-এর কাছে কেবল এই ডেটাই আছে। ব্যবহারকারীরা সাধারণত বাইট ভ্যালু ফেরত চান না, এমনকি যদি স্ট্রিংটিতে শুধুমাত্র ল্যাটিন অক্ষর থাকে: যদি &"hi"[0] বৈধ কোড হতো যা বাইট ভ্যালু ফেরত দিত, তবে এটি h-এর পরিবর্তে 104 ফেরত দিত।

উত্তরটি হলো, একটি অপ্রত্যাশিত মান ফেরত দেওয়া এবং এমন বাগ তৈরি করা এড়াতে যা অবিলম্বে আবিষ্কৃত নাও হতে পারে, Rust এই কোডটি মোটেই কম্পাইল করে না এবং ডেভেলপমেন্ট প্রক্রিয়ার শুরুতেই ভুল বোঝাবুঝি প্রতিরোধ করে।

বাইট, স্কেলার ভ্যালু এবং গ্রাফিম ক্লাস্টার! এলাহি কাণ্ড!

UTF-8 সম্পর্কে আরেকটি বিষয় হলো যে Rust-এর দৃষ্টিকোণ থেকে স্ট্রিং দেখার তিনটি প্রাসঙ্গিক উপায় রয়েছে: বাইট হিসাবে, স্কেলার ভ্যালু হিসাবে, এবং গ্রাফিম ক্লাস্টার হিসাবে (যাকে আমরা অক্ষর বলি তার সবচেয়ে কাছের জিনিস)।

যদি আমরা দেবনাগরী লিপিতে লেখা হিন্দি শব্দ "नमस्ते" দেখি, এটি u8 ভ্যালুর একটি ভেক্টর হিসাবে স্টোর করা হয় যা দেখতে এইরকম:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

এটি ১৮ বাইট এবং কম্পিউটারগুলি এভাবেই ডেটা সঞ্চয় করে। যদি আমরা এগুলিকে ইউনিকোড স্কেলার ভ্যালু হিসাবে দেখি, যা Rust-এর char টাইপ, তবে সেই বাইটগুলি দেখতে এইরকম:

['न', 'म', 'स', '्', 'त', 'े']

এখানে ছয়টি char ভ্যালু রয়েছে, কিন্তু চতুর্থ এবং ষষ্ঠটি অক্ষর নয়: সেগুলি ডায়াক্রিটিক যা নিজে থেকে অর্থপূর্ণ নয়। অবশেষে, যদি আমরা এগুলিকে গ্রাফিম ক্লাস্টার হিসাবে দেখি, তবে আমরা সেই চারটি অক্ষর পাব যা একজন ব্যক্তি হিন্দি শব্দটি তৈরি করতে ব্যবহার করবে:

["न", "म", "स्", "ते"]

Rust কম্পিউটার দ্বারা সংরক্ষিত কাঁচা স্ট্রিং ডেটা ব্যাখ্যা করার বিভিন্ন উপায় সরবরাহ করে যাতে প্রতিটি প্রোগ্রাম তার প্রয়োজনীয় ব্যাখ্যা বেছে নিতে পারে, ডেটা যে কোনো মানব ভাষায়ই হোক না কেন।

Rust আমাদের একটি অক্ষর পেতে একটি String-এ ইনডেক্স করার অনুমতি না দেওয়ার একটি চূড়ান্ত কারণ হলো যে ইনডেক্সিং অপারেশনগুলি সর্বদা ধ্রুবক সময়ে (O(1)) সম্পন্ন হবে বলে আশা করা হয়। কিন্তু একটি String-এর সাথে সেই পারফরম্যান্সের গ্যারান্টি দেওয়া সম্ভব নয়, কারণ Rust-কে শুরু থেকে ইনডেক্স পর্যন্ত বিষয়বস্তুর মধ্যে দিয়ে হেঁটে যেতে হবে কতগুলি বৈধ অক্ষর ছিল তা নির্ধারণ করার জন্য।

স্ট্রিং স্লাইস করা

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

একটি একক সংখ্যা দিয়ে [] ব্যবহার করে ইনডেক্স করার পরিবর্তে, আপনি নির্দিষ্ট বাইট ধারণকারী একটি স্ট্রিং স্লাইস তৈরি করতে একটি পরিসীমা (range) সহ [] ব্যবহার করতে পারেন:

#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
}

এখানে, s একটি &str হবে যা স্ট্রিং-এর প্রথম চারটি বাইট ধারণ করে। আগে, আমরা উল্লেখ করেছি যে এই অক্ষরগুলির প্রতিটি দুই বাইট করে ছিল, যার মানে s হবে Зд

যদি আমরা &hello[0..1]-এর মতো কিছু দিয়ে একটি অক্ষরের বাইটের কেবল একটি অংশ স্লাইস করার চেষ্টা করতাম, তাহলে Rust রানটাইমে প্যানিক করত, যেমন একটি ভেক্টরে একটি অবৈধ ইনডেক্স অ্যাক্সেস করা হলে হয়:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/collections`

thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

রেঞ্জ ব্যবহার করে স্ট্রিং স্লাইস তৈরি করার সময় আপনার সতর্কতা অবলম্বন করা উচিত, কারণ এটি আপনার প্রোগ্রাম ক্র্যাশ করতে পারে।

স্ট্রিং-এর উপর ইটারেট করার মেথড

স্ট্রিং-এর অংশে কাজ করার সেরা উপায় হলো আপনি অক্ষর চান নাকি বাইট চান সে সম্পর্কে স্পষ্ট হওয়া। স্বতন্ত্র ইউনিকোড স্কেলার ভ্যালুর জন্য, chars মেথড ব্যবহার করুন। "Зд"-এর উপর chars কল করা হলে এটি দুটি char টাইপের ভ্যালু আলাদা করে ফেরত দেয়, এবং আপনি প্রতিটি এলিমেন্ট অ্যাক্সেস করতে ফলাফলের উপর ইটারেট করতে পারেন:

#![allow(unused)]
fn main() {
for c in "Зд".chars() {
    println!("{c}");
}
}

এই কোডটি নিম্নলিখিত আউটপুট প্রিন্ট করবে:

З
д

বিকল্পভাবে, bytes মেথড প্রতিটি কাঁচা বাইট ফেরত দেয়, যা আপনার ডোমেনের জন্য উপযুক্ত হতে পারে:

#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
    println!("{b}");
}
}

এই কোডটি এই স্ট্রিংটি গঠনকারী চারটি বাইট প্রিন্ট করবে:

208
151
208
180

তবে মনে রাখতে ভুলবেন না যে বৈধ ইউনিকোড স্কেলার ভ্যালু একাধিক বাইট দিয়ে গঠিত হতে পারে।

দেবনাগরী লিপির মতো স্ট্রিং থেকে গ্রাফিম ক্লাস্টার পাওয়া জটিল, তাই এই কার্যকারিতা standard library দ্বারা সরবরাহ করা হয় না। যদি আপনার এই কার্যকারিতার প্রয়োজন হয়, তাহলে crates.io-তে ক্রেট উপলব্ধ আছে।

স্ট্রিং অতটা সহজ নয়

সংক্ষেপে, স্ট্রিং বেশ জটিল। বিভিন্ন প্রোগ্রামিং ল্যাঙ্গুয়েজ প্রোগ্রামারদের কাছে এই জটিলতা কীভাবে উপস্থাপন করা হবে সে সম্পর্কে বিভিন্ন সিদ্ধান্ত নেয়। Rust String ডেটার সঠিক হ্যান্ডলিং-কে সমস্ত Rust প্রোগ্রামের জন্য ডিফল্ট আচরণ হিসাবে বেছে নিয়েছে, যার মানে প্রোগ্রামারদের UTF-8 ডেটা হ্যান্ডলিং নিয়ে আগে থেকেই আরও বেশি ভাবতে হবে। এই ট্রেড-অফটি অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজে দৃশ্যমান স্ট্রিং-এর জটিলতার চেয়ে বেশি প্রকাশ করে, তবে এটি আপনাকে আপনার ডেভেলপমেন্ট লাইফ সাইকেলের পরবর্তী পর্যায়ে নন-ASCII অক্ষর সম্পর্কিত এরর হ্যান্ডলিং করা থেকে বিরত রাখে।

সুখবর হলো, standard library String এবং &str টাইপের উপর ভিত্তি করে অনেক কার্যকারিতা সরবরাহ করে যা এই জটিল পরিস্থিতিগুলি সঠিকভাবে পরিচালনা করতে সহায়তা করে। স্ট্রিং-এ অনুসন্ধানের জন্য contains এবং স্ট্রিং-এর অংশ অন্য স্ট্রিং দিয়ে প্রতিস্থাপনের জন্য replace-এর মতো দরকারি মেথডগুলির জন্য ডকুমেন্টেশন দেখতে ভুলবেন না।

চলুন এবার একটু কম জটিল কিছুতে যাওয়া যাক: hash maps