স্ট্রিং দিয়ে UTF-8 এনকোডেড টেক্সট সংরক্ষণ করা (Storing UTF-8 Encoded Text with Strings)
আমরা চ্যাপ্টার ৪-এ স্ট্রিং নিয়ে কথা বলেছি, কিন্তু এখন আমরা সেগুলোর দিকে আরও গভীরভাবে দেখব। নতুন Rustacean-রা সাধারণত স্ট্রিং নিয়ে সমস্যায় পড়েন তিনটি কারণে: Rust-এর সম্ভাব্য এররগুলো প্রকাশ করার প্রবণতা, স্ট্রিং অনেকের ধারণার চেয়ে বেশি জটিল ডেটা স্ট্রাকচার এবং UTF-8। এই বিষয়গুলো একত্রিত হয়ে এমন একটি পরিস্থিতির সৃষ্টি করে যা অন্যান্য প্রোগ্রামিং ভাষা থেকে আসা লোকেদের কাছে কঠিন মনে হতে পারে।
আমরা কালেকশনের পরিপ্রেক্ষিতে স্ট্রিং নিয়ে আলোচনা করি কারণ স্ট্রিংগুলো বাইটের একটি কালেকশন হিসাবে প্রয়োগ করা হয়, সেইসাথে কিছু মেথড যা সেই বাইটগুলোকে টেক্সট হিসাবে ব্যাখ্যা করা হলে দরকারী কার্যকারিতা প্রদান করে। এই বিভাগে, আমরা String
-এর অপারেশনগুলো নিয়ে কথা বলব যা প্রতিটি কালেকশন টাইপের আছে, যেমন তৈরি করা, আপডেট করা এবং পড়া। আমরা আরও আলোচনা করব কিভাবে String
অন্যান্য কালেকশন থেকে আলাদা, অর্থাৎ কিভাবে একটি String
-এ ইনডেক্সিং করা মানুষের এবং কম্পিউটারের String
ডেটা ব্যাখ্যা করার পার্থক্যের কারণে জটিল।
স্ট্রিং কী? (What Is a String?)
আমরা প্রথমে স্ট্রিং (string) বলতে কী বোঝায় তা সংজ্ঞায়িত করব। Rust-এর কোর ল্যাঙ্গুয়েজে শুধুমাত্র একটি স্ট্রিং টাইপ রয়েছে, সেটি হল স্ট্রিং স্লাইস str
যা সাধারণত এর বোরোড (borrowed) ফর্ম &str
-এ দেখা যায়। চ্যাপ্টার ৪-এ, আমরা স্ট্রিং স্লাইস নিয়ে কথা বলেছি, যেগুলো অন্য কোথাও সংরক্ষিত কিছু UTF-8 এনকোডেড স্ট্রিং ডেটার রেফারেন্স। উদাহরণস্বরূপ, স্ট্রিং লিটারেলগুলো প্রোগ্রামের বাইনারিতে সংরক্ষণ করা হয় এবং তাই সেগুলো স্ট্রিং স্লাইস।
String
টাইপ, যা Rust-এর স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা হয়, কোর ল্যাঙ্গুয়েজের মধ্যে কোড করা হয়নি, এটি একটি প্রসারণযোগ্য, পরিবর্তনযোগ্য, ওনড (owned), UTF-8 এনকোডেড স্ট্রিং টাইপ। যখন Rustacean-রা Rust-এ "স্ট্রিং" উল্লেখ করে, তখন তারা String
বা স্ট্রিং স্লাইস &str
টাইপ উভয়কেই বোঝাতে পারে, শুধু একটি টাইপকে নয়। যদিও এই বিভাগটি মূলত String
সম্পর্কে, উভয় টাইপই Rust-এর স্ট্যান্ডার্ড লাইব্রেরিতে প্রচুর ব্যবহৃত হয় এবং String
ও স্ট্রিং স্লাইস উভয়ই UTF-8 এনকোডেড।
একটি নতুন স্ট্রিং তৈরি করা (Creating a New String)
Vec<T>
-এর সাথে উপলব্ধ অনেকগুলি অপারেশন String
-এর সাথেও উপলব্ধ, কারণ String
আসলে কিছু অতিরিক্ত গ্যারান্টি, সীমাবদ্ধতা এবং ক্ষমতা সহ বাইটের একটি ভেক্টরের চারপাশে একটি র্যাপার (wrapper) হিসাবে প্রয়োগ করা হয়। Vec<T>
এবং String
-এর সাথে একইভাবে কাজ করে এমন একটি ফাংশনের উদাহরণ হল একটি ইন্সট্যান্স তৈরি করার জন্য new
ফাংশন, যা Listing 8-11-তে দেখানো হয়েছে।
fn main() { let mut s = String::new(); }
এই লাইনটি s
নামে একটি নতুন, খালি স্ট্রিং তৈরি করে, যেখানে আমরা ডেটা লোড করতে পারি। প্রায়শই, আমাদের কাছে কিছু প্রাথমিক ডেটা থাকবে যা দিয়ে আমরা স্ট্রিং শুরু করতে চাই। এর জন্য, আমরা to_string
মেথড ব্যবহার করি, যা যেকোনো টাইপে উপলব্ধ যা Display
ট্রেইট ইমপ্লিমেন্ট করে, যেমনটি স্ট্রিং লিটারেলগুলো করে। Listing 8-12 দুটি উদাহরণ দেখায়।
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
তৈরি করতে String::from
ফাংশনটিও ব্যবহার করতে পারি। Listing 8-13-এর কোডটি Listing 8-12-এর কোডের সমতুল্য যা to_string
ব্যবহার করে।
fn main() { let s = String::from("initial contents"); }
যেহেতু স্ট্রিংগুলো অনেক কিছুর জন্য ব্যবহার করা হয়, তাই আমরা স্ট্রিংয়ের জন্য অনেকগুলি ভিন্ন জেনেরিক API ব্যবহার করতে পারি, যা আমাদের অনেক অপশন সরবরাহ করে। এগুলোর মধ্যে কিছু অপ্রয়োজনীয় মনে হতে পারে, তবে সবারই নিজস্ব স্থান রয়েছে! এই ক্ষেত্রে, String::from
এবং to_string
একই কাজ করে, তাই আপনি কোনটি বেছে নেবেন তা স্টাইল এবং পঠনযোগ্যতার উপর নির্ভর করে।
মনে রাখবেন যে স্ট্রিংগুলো UTF-8 এনকোডেড, তাই আমরা সেগুলোর মধ্যে যেকোনো সঠিকভাবে এনকোড করা ডেটা অন্তর্ভুক্ত করতে পারি, যেমনটি Listing 8-14-তে দেখানো হয়েছে।
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
মান।
একটি স্ট্রিং আপডেট করা (Updating a String)
একটি String
আকারে বাড়তে পারে এবং এর কনটেন্ট পরিবর্তন হতে পারে, ঠিক Vec<T>
-এর কনটেন্টের মতোই, যদি আপনি এতে আরও ডেটা পুশ করেন। এছাড়াও, আপনি সুবিধাজনকভাবে +
অপারেটর বা format!
ম্যাক্রো ব্যবহার করে String
মানগুলোকে সংযুক্ত করতে পারেন।
push_str
এবং push
দিয়ে একটি স্ট্রিং-এ যুক্ত করা (Appending to a String with push_str
and push
)
আমরা একটি স্ট্রিং স্লাইস যুক্ত করতে push_str
মেথড ব্যবহার করে একটি String
বাড়াতে পারি, যেমনটি Listing 8-15-তে দেখানো হয়েছে।
fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
এই দুটি লাইনের পরে, s
-এ foobar
থাকবে। push_str
মেথডটি একটি স্ট্রিং স্লাইস নেয় কারণ আমরা অপরিহার্যভাবে প্যারামিটারের ওনারশিপ নিতে চাই না। উদাহরণস্বরূপ, Listing 8-16-এর কোডে, আমরা s1
-এ এর কনটেন্ট যুক্ত করার পরে s2
ব্যবহার করতে সক্ষম হতে চাই।
fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {s2}"); }
যদি push_str
মেথডটি s2
-এর ওনারশিপ নিত, তাহলে আমরা শেষ লাইনে এর মান প্রিন্ট করতে পারতাম না। যাইহোক, এই কোডটি আমাদের প্রত্যাশা অনুযায়ী কাজ করে!
push
মেথডটি একটি একক অক্ষরকে প্যারামিটার হিসাবে নেয় এবং এটিকে String
-এ যুক্ত করে। Listing 8-17 push
মেথড ব্যবহার করে একটি String
-এ l অক্ষর যোগ করে।
fn main() { let mut s = String::from("lo"); s.push('l'); }
ফলস্বরূপ, s
-এ lol
থাকবে।
+
অপারেটর বা format!
ম্যাক্রো দিয়ে কনক্যাটেনেশন (Concatenation with the +
Operator or the format!
Macro)
প্রায়শই, আপনি দুটি বিদ্যমান স্ট্রিংকে একত্রিত করতে চাইবেন। এটি করার একটি উপায় হল +
অপারেটর ব্যবহার করা, যেমনটি Listing 8-18-এ দেখানো হয়েছে।
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 {
স্ট্যান্ডার্ড লাইব্রেরিতে, আপনি add
কে জেনেরিক এবং অ্যাসোসিয়েটেড টাইপ ব্যবহার করে সংজ্ঞায়িত দেখতে পাবেন। এখানে, আমরা কংক্রিট টাইপগুলোতে প্রতিস্থাপিত করেছি, যা ঘটে যখন আমরা এই মেথডটিকে String
মান দিয়ে কল করি। আমরা চ্যাপ্টার ১০-এ জেনেরিক নিয়ে আলোচনা করব। এই সিগনেচারটি আমাদের +
অপারেটরের জটিল বিটগুলো বোঝার জন্য প্রয়োজনীয় ক্লু দেয়।
প্রথমত, s2
-এর একটি &
রয়েছে, যার অর্থ হল আমরা দ্বিতীয় স্ট্রিংটির একটি রেফারেন্স প্রথম স্ট্রিংটিতে যুক্ত করছি। এটি add
ফাংশনের s
প্যারামিটারের কারণে: আমরা শুধুমাত্র একটি String
-এর সাথে একটি &str
যোগ করতে পারি; আমরা দুটি String
মান একসাথে যোগ করতে পারি না। কিন্তু অপেক্ষা করুন—&s2
-এর টাইপ হল &String
, &str
নয়, যেমনটি add
-এর দ্বিতীয় প্যারামিটারে নির্দিষ্ট করা হয়েছে। তাহলে Listing 8-18 কেন কম্পাইল হয়?
আমরা add
কলে &s2
ব্যবহার করতে সক্ষম হওয়ার কারণ হল কম্পাইলার &String
আর্গুমেন্টটিকে একটি &str
-এ কোয়ার্স (coerce) করতে পারে। যখন আমরা add
মেথডটি কল করি, তখন Rust একটি ডিরেফ কোয়েরশন (deref coercion) ব্যবহার করে, যা এখানে &s2
কে &s2[..]
-তে পরিণত করে। আমরা চ্যাপ্টার ১৫-এ ডিরেফ কোয়েরশন নিয়ে আরও বিস্তারিত আলোচনা করব। যেহেতু add
s
প্যারামিটারের ওনারশিপ নেয় না, তাই s2
এই অপারেশনের পরেও একটি বৈধ String
থাকবে।
দ্বিতীয়ত, আমরা সিগনেচারে দেখতে পাচ্ছি যে add
self
-এর ওনারশিপ নেয় কারণ self
-এর &
নেই। এর মানে Listing 8-18-এর s1
add
কলে সরানো হবে এবং তার পরে আর বৈধ থাকবে না। সুতরাং, যদিও let s3 = s1 + &s2;
দেখে মনে হচ্ছে এটি উভয় স্ট্রিং কপি করবে এবং একটি নতুন তৈরি করবে, এই স্টেটমেন্টটি আসলে s1
-এর ওনারশিপ নেয়, s2
-এর কনটেন্টের একটি কপি যুক্ত করে এবং তারপর ফলাফলের ওনারশিপ ফিরিয়ে দেয়। অন্য কথায়, এটি দেখতে অনেকগুলো কপি তৈরি করার মতো, কিন্তু তা নয়; ইমপ্লিমেন্টেশনটি কপি করার চেয়ে বেশি কার্যকরী।
যদি আমাদের একাধিক স্ট্রিংকে কনক্যাটেনেট করতে হয়, তাহলে +
অপারেটরের আচরণ জটিল হয়ে যায়:
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!
ম্যাক্রো দ্বারা জেনারেট করা কোড রেফারেন্স ব্যবহার করে যাতে এই কলটি এর কোনো প্যারামিটারের ওনারশিপ না নেয়।
স্ট্রিংগুলোতে ইনডেক্সিং (Indexing into Strings)
অন্যান্য অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজে, ইনডেক্সের মাধ্যমে রেফারেন্স করে একটি স্ট্রিংয়ের পৃথক অক্ষরগুলো অ্যাক্সেস করা একটি বৈধ এবং সাধারণ অপারেশন। যাইহোক, আপনি যদি Rust-এ ইনডেক্সিং সিনট্যাক্স ব্যবহার করে একটি String
-এর অংশগুলো অ্যাক্সেস করার চেষ্টা করেন, তাহলে আপনি একটি এরর পাবেন। Listing 8-19-এর অবৈধ কোডটি বিবেচনা করুন।
fn main() {
let s1 = String::from("hello");
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>
-এর উপর একটি র্যাপার। Listing 8-14 থেকে আমাদের সঠিকভাবে এনকোড করা 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"
স্ট্রিংটি সংরক্ষণ করা ভেক্টরটি 4 বাইট লম্বা। 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"); }
আপনাকে যদি জিজ্ঞাসা করা হয় স্ট্রিংটি কত লম্বা, তাহলে আপনি হয়তো বলবেন 12। আসলে, Rust-এর উত্তর হল 24: UTF-8-এ “Здравствуйте” এনকোড করতে যত বাইট লাগে, কারণ সেই স্ট্রিংয়ের প্রতিটি ইউনিকোড স্কেলার মান 2 বাইট স্টোরেজ নেয়। অতএব, স্ট্রিংয়ের বাইটগুলোতে একটি ইনডেক্স সর্বদাই একটি বৈধ ইউনিকোড স্কেলার মানের সাথে সম্পর্কযুক্ত হবে না। এটি প্রদর্শন করতে, এই অবৈধ Rust কোডটি বিবেচনা করুন:
let hello = "Здравствуйте";
let answer = &hello[0];
আপনি ইতিমধ্যেই জানেন যে answer
З
হবে না, প্রথম অক্ষর। UTF-8-এ এনকোড করা হলে, З
-এর প্রথম বাইট হল 208
এবং দ্বিতীয়টি হল 151
, তাই মনে হতে পারে যে answer
আসলে 208
হওয়া উচিত, কিন্তু 208
নিজে থেকে একটি বৈধ অক্ষর নয়। 208
রিটার্ন করা সম্ভবত ব্যবহারকারী যা চাইবেন তা নয় যদি তারা এই স্ট্রিংটির প্রথম অক্ষরটি জিজ্ঞাসা করে; তবে, Rust-এর কাছে বাইট ইনডেক্স 0-তে সেই ডেটাই রয়েছে। ব্যবহারকারীরা সাধারণত বাইট মান রিটার্ন চান না, এমনকী যদি স্ট্রিংটিতে শুধুমাত্র ল্যাটিন অক্ষর থাকে: যদি &"hi"[0]
বৈধ কোড হত যা বাইট মান রিটার্ন করত, তাহলে এটি h
নয়, 104
রিটার্ন করত।
তাহলে, উত্তর হল, একটি অপ্রত্যাশিত মান রিটার্ন করা এবং বাগগুলো এড়াতে যা অবিলম্বে আবিষ্কার নাও হতে পারে, Rust এই কোডটি কম্পাইল করে না এবং ডেভেলপমেন্ট প্রক্রিয়ার শুরুতেই ভুল বোঝাবুঝি প্রতিরোধ করে।
বাইট এবং স্কেলার মান এবং গ্রাফিম ক্লাস্টার! ওহ মাই! (Bytes and Scalar Values and Grapheme Clusters! Oh My!)
UTF-8 সম্পর্কে আরেকটি বিষয় হল যে Rust-এর দৃষ্টিকোণ থেকে স্ট্রিংগুলো দেখার জন্য আসলে তিনটি প্রাসঙ্গিক উপায় রয়েছে: বাইট হিসাবে, স্কেলার মান হিসাবে এবং গ্রাফিম ক্লাস্টার হিসাবে (আমরা যাকে অক্ষর বলব তার সবচেয়ে কাছের জিনিস)।
আমরা যদি দেবনাগরী লিপিতে লেখা হিন্দি শব্দ “नमस्ते”-এর দিকে তাকাই, তাহলে এটি u8
মানগুলোর একটি ভেক্টর হিসাবে সংরক্ষিত হয় যা দেখতে এইরকম:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
এটি 18 বাইট এবং কম্পিউটারগুলো শেষ পর্যন্ত এই ডেটা এভাবেই সংরক্ষণ করে। আমরা যদি সেগুলোকে ইউনিকোড স্কেলার মান হিসাবে দেখি, যেগুলো Rust-এর char
টাইপ, তাহলে সেই বাইটগুলো দেখতে এইরকম:
['न', 'म', 'स', '्', 'त', 'े']
এখানে ছয়টি char
মান রয়েছে, কিন্তু চতুর্থ এবং ষষ্ঠটি অক্ষর নয়: সেগুলো হল ডায়াক্রিটিকস (diacritics) যেগুলোর নিজস্ব কোনো অর্থ নেই। অবশেষে, যদি আমরা সেগুলোকে গ্রাফিম ক্লাস্টার হিসাবে দেখি, তাহলে আমরা একজন ব্যক্তি যাকে হিন্দি শব্দটি তৈরি করা চারটি অক্ষর বলবে তা পাব:
["न", "म", "स्", "ते"]
Rust কম্পিউটারগুলোর সংরক্ষণ করা কাঁচা স্ট্রিং ডেটা ব্যাখ্যা করার বিভিন্ন উপায় সরবরাহ করে যাতে প্রতিটি প্রোগ্রাম তার প্রয়োজনীয় ব্যাখ্যাটি বেছে নিতে পারে, ডেটাটি কোন মানব ভাষা হোক না কেন।
Rust আমাদের একটি অক্ষর পাওয়ার জন্য একটি String
-এ ইনডেক্স করার অনুমতি দেয় না তার একটি শেষ কারণ হল ইনডেক্সিং অপারেশনগুলো সর্বদাই কনস্ট্যান্ট টাইমে (O(1)) নেওয়ার আশা করা হয়। কিন্তু একটি String
দিয়ে সেই পারফরম্যান্স গ্যারান্টি দেওয়া সম্ভব নয়, কারণ Rust-কে শুরু থেকে ইনডেক্স পর্যন্ত কনটেন্টের মধ্যে দিয়ে যেতে হবে যাতে কতগুলো বৈধ অক্ষর ছিল তা নির্ধারণ করতে হয়।
স্ট্রিং স্লাইসিং (Slicing Strings)
একটি স্ট্রিং-এ ইনডেক্সিং করা প্রায়শই একটি খারাপ ধারণা কারণ এটি স্পষ্ট নয় যে স্ট্রিং-ইনডেক্সিং অপারেশনের রিটার্ন টাইপ কী হওয়া উচিত: একটি বাইট মান, একটি অক্ষর, একটি গ্রাফিম ক্লাস্টার বা একটি স্ট্রিং স্লাইস। অতএব, যদি আপনার সত্যিই স্ট্রিং স্লাইস তৈরি করতে ইনডেক্স ব্যবহার করার প্রয়োজন হয়, তাহলে Rust আপনাকে আরও নির্দিষ্ট হতে বলে।
একটি একক সংখ্যা সহ []
ব্যবহার করে ইনডেক্সিং করার পরিবর্তে, আপনি নির্দিষ্ট বাইট ধারণকারী একটি স্ট্রিং স্লাইস তৈরি করতে একটি রেঞ্জ সহ []
ব্যবহার করতে পারেন:
#![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
রেঞ্জ দিয়ে স্ট্রিং স্লাইস তৈরি করার সময় আপনার সতর্কতা অবলম্বন করা উচিত, কারণ এটি করলে আপনার প্রোগ্রাম ক্র্যাশ করতে পারে।
স্ট্রিংগুলোর উপর ইটারেট করার মেথড (Methods for Iterating Over Strings)
স্ট্রিং-এর অংশগুলোতে কাজ করার সর্বোত্তম উপায় হল আপনি অক্ষর চান নাকি বাইট চান সে সম্পর্কে স্পষ্ট হওয়া। পৃথক ইউনিকোড স্কেলার মানগুলোর জন্য, 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
কিন্তু মনে রাখতে ভুলবেন না যে বৈধ ইউনিকোড স্কেলার মানগুলো একাধিক বাইট দিয়ে তৈরি হতে পারে।
স্ট্রিং থেকে গ্রাফিম ক্লাস্টার পাওয়া, যেমন দেবনাগরী লিপির সাথে, জটিল, তাই এই কার্যকারিতা স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা হয় না। আপনার যদি এই কার্যকারিতার প্রয়োজন হয় তবে crates.io-তে ক্রেট উপলব্ধ রয়েছে।
স্ট্রিংগুলো এত সহজ নয় (Strings Are Not So Simple)
সংক্ষেপে, স্ট্রিংগুলো জটিল। বিভিন্ন প্রোগ্রামিং ভাষাগুলো প্রোগ্রামারের কাছে এই জটিলতা কীভাবে উপস্থাপন করতে হয় সে সম্পর্কে বিভিন্ন পছন্দ করে। Rust সমস্ত Rust প্রোগ্রামের জন্য String
ডেটার সঠিক হ্যান্ডলিংকে ডিফল্ট আচরণ হিসাবে তৈরি করতে বেছে নিয়েছে, যার অর্থ হল প্রোগ্রামারদের সামনে UTF-8 ডেটা হ্যান্ডেল করার বিষয়ে আরও বেশি চিন্তা করতে হবে। এই ট্রেড-অফটি অন্যান্য প্রোগ্রামিং ভাষাগুলোর তুলনায় স্ট্রিংগুলোর আরও জটিলতা প্রকাশ করে, তবে এটি আপনাকে আপনার ডেভেলপমেন্ট লাইফ সাইকেলের পরে নন-ASCII অক্ষর জড়িত এররগুলো হ্যান্ডেল করা থেকে বিরত রাখে।
ভাল খবর হল স্ট্যান্ডার্ড লাইব্রেরি এই জটিল পরিস্থিতিগুলোকে সঠিকভাবে পরিচালনা করতে সহায়তা করার জন্য String
এবং &str
টাইপের উপর নির্মিত প্রচুর কার্যকারিতা সরবরাহ করে। স্ট্রিং-এ অনুসন্ধানের জন্য contains
-এর মতো এবং একটি স্ট্রিং-এর অংশগুলোকে অন্য স্ট্রিং দিয়ে প্রতিস্থাপন করার জন্য replace
-এর মতো দরকারী মেথডগুলোর জন্য ডকুমেন্টেশন পরীক্ষা করতে ভুলবেন না।
আসুন একটু কম জটিল কিছুতে যাই: হ্যাশ ম্যাপ!