স্লাইস টাইপ (The Slice Type)

স্লাইস (slices) আপনাকে সম্পূর্ণ কালেকশনের পরিবর্তে কালেকশনের উপাদানগুলোর একটি ধারাবাহিক অংশকে রেফারেন্স করতে দেয়। একটি স্লাইস হল এক ধরনের রেফারেন্স, তাই এর ওনারশিপ নেই।

এখানে একটি ছোট প্রোগ্রামিং সমস্যা রয়েছে: এমন একটি ফাংশন লিখুন যা স্পেস দ্বারা পৃথক করা শব্দের একটি স্ট্রিং নেয় এবং সেই স্ট্রিংটিতে পাওয়া প্রথম শব্দটি রিটার্ন করে। যদি ফাংশনটি স্ট্রিংটিতে কোনো স্পেস খুঁজে না পায়, তাহলে পুরো স্ট্রিংটি অবশ্যই একটি শব্দ হবে, তাই সম্পূর্ণ স্ট্রিংটি রিটার্ন করা উচিত।

স্লাইস ব্যবহার না করে আমরা কীভাবে এই ফাংশনের সিগনেচার লিখব তা নিয়ে কাজ করি, যাতে স্লাইসগুলো যে সমস্যার সমাধান করবে তা বোঝা যায়:

fn first_word(s: &String) -> ?

first_word ফাংশনটির প্যারামিটার হিসাবে একটি &String রয়েছে। আমাদের ওনারশিপের প্রয়োজন নেই, তাই এটি ঠিক আছে। (প্রচলিত Rust-এ, ফাংশনগুলো তাদের আর্গুমেন্টের ওনারশিপ নেয় না যদি না তাদের প্রয়োজন হয়, এবং এর কারণগুলো আমরা যত এগোব ততই স্পষ্ট হয়ে উঠবে!) কিন্তু আমরা কী রিটার্ন করব? আমাদের কাছে স্ট্রিং-এর অংশ সম্পর্কে কথা বলার কোনো উপায় নেই। তবে, আমরা একটি স্পেস দ্বারা নির্দেশিত শব্দের শেষের ইনডেক্সটি রিটার্ন করতে পারি। চলুন Listing 4-7-এ দেখানো মতো সেটি চেষ্টা করি।

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

যেহেতু আমাদের String-এর প্রতিটি এলিমেন্টের মধ্যে গিয়ে পরীক্ষা করতে হবে যে কোনো মান স্পেস কিনা, তাই আমরা as_bytes মেথড ব্যবহার করে আমাদের String-কে বাইটের অ্যারেতে রূপান্তর করব।

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

এরপর, আমরা iter মেথড ব্যবহার করে বাইটের অ্যারের উপর একটি ইটারেটর তৈরি করি:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

আমরা চ্যাপ্টার 13-এ ইটারেটরগুলো নিয়ে আরও বিস্তারিত আলোচনা করব। আপাতত, জেনে রাখুন যে iter হল একটি মেথড যা একটি কালেকশনের প্রতিটি এলিমেন্ট রিটার্ন করে এবং enumerate iter-এর ফলাফলকে র‍্যাপ করে এবং প্রতিটি এলিমেন্টকে একটি টাপলের অংশ হিসাবে রিটার্ন করে। enumerate থেকে রিটার্ন করা টাপলের প্রথম এলিমেন্টটি হল ইনডেক্স এবং দ্বিতীয় এলিমেন্টটি হল এলিমেন্টের একটি রেফারেন্স। এটি নিজে থেকে ইনডেক্স গণনা করার চেয়ে একটু বেশি সুবিধাজনক।

যেহেতু enumerate মেথড একটি টাপল রিটার্ন করে, তাই আমরা সেই টাপলটিকে ডিস্ট্রাকচার করতে প্যাটার্ন ব্যবহার করতে পারি। আমরা চ্যাপ্টার 6-এ প্যাটার্নগুলো নিয়ে আরও আলোচনা করব। for লুপে, আমরা একটি প্যাটার্ন নির্দিষ্ট করি যেখানে টাপলের ইনডেক্সের জন্য i এবং টাপলের একক বাইটের জন্য &item রয়েছে। যেহেতু আমরা .iter().enumerate() থেকে এলিমেন্টের একটি রেফারেন্স পাই, তাই আমরা প্যাটার্নে & ব্যবহার করি।

for লুপের ভিতরে, আমরা বাইট লিটারেল সিনট্যাক্স ব্যবহার করে স্পেস উপস্থাপন করে এমন বাইটটি খুঁজি। যদি আমরা একটি স্পেস খুঁজে পাই, তাহলে আমরা অবস্থানটি রিটার্ন করি। অন্যথায়, আমরা s.len() ব্যবহার করে স্ট্রিংটির দৈর্ঘ্য রিটার্ন করি।

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

আমাদের কাছে এখন স্ট্রিং-এর প্রথম শব্দের শেষের ইনডেক্স খুঁজে বের করার একটি উপায় আছে, কিন্তু একটি সমস্যা আছে। আমরা নিজে থেকে একটি usize রিটার্ন করছি, কিন্তু এটি শুধুমাত্র &String-এর পরিপ্রেক্ষিতে একটি অর্থপূর্ণ সংখ্যা। অন্য কথায়, যেহেতু এটি String থেকে একটি পৃথক মান, তাই ভবিষ্যতে এটি বৈধ থাকবে এমন কোনো নিশ্চয়তা নেই। Listing 4-8-এর প্রোগ্রামটি বিবেচনা করুন, যেটি Listing 4-7 থেকে first_word ফাংশন ব্যবহার করে।

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // `word` still has the value `5` here, but `s` no longer has any content
    // that we could meaningfully use with the value `5`, so `word` is now
    // totally invalid!
}

এই প্রোগ্রামটি কোনো এরর ছাড়াই কম্পাইল হয় এবং s.clear() কল করার পরেও যদি আমরা word ব্যবহার করি তাহলেও কম্পাইল হবে। যেহেতু word কোনোভাবেই s-এর অবস্থার সাথে সংযুক্ত নয়, তাই word-এ এখনও 5 মান রয়েছে। আমরা সেই 5 মানটি s ভেরিয়েবলের সাথে ব্যবহার করে প্রথম শব্দটি বের করার চেষ্টা করতে পারি, কিন্তু এটি একটি বাগ হবে কারণ আমরা word-এ 5 সংরক্ষণ করার পর থেকে s-এর কনটেন্ট পরিবর্তন হয়েছে।

word-এর ইনডেক্সটি s-এর ডেটার সাথে সিঙ্কের বাইরে চলে যাওয়া নিয়ে চিন্তা করা ক্লান্তিকর এবং এরর-প্রবণ! আমরা যদি একটি second_word ফাংশন লিখি তাহলে এই ইনডেক্সগুলো পরিচালনা করা আরও কঠিন। এর সিগনেচারটি এমন হতে হবে:

fn second_word(s: &String) -> (usize, usize) {

এখন আমরা একটি শুরুর এবং একটি শেষের ইনডেক্স ট্র্যাক করছি এবং আমাদের কাছে আরও বেশি মান রয়েছে যা একটি নির্দিষ্ট অবস্থার ডেটা থেকে গণনা করা হয়েছে কিন্তু সেই অবস্থার সাথে কোনোভাবেই সংযুক্ত নয়। আমাদের কাছে তিনটি সম্পর্কহীন ভেরিয়েবল রয়েছে যেগুলোকে সিঙ্কে রাখতে হবে।

সৌভাগ্যবশত, Rust-এর এই সমস্যার একটি সমাধান রয়েছে: স্ট্রিং স্লাইস।

স্ট্রিং স্লাইস (String Slices)

একটি স্ট্রিং স্লাইস হল একটি String-এর অংশের একটি রেফারেন্স, এবং এটি দেখতে এরকম:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

সম্পূর্ণ String-এর রেফারেন্সের পরিবর্তে, hello হল String-এর একটি অংশের রেফারেন্স, যা অতিরিক্ত [0..5] বিটে নির্দিষ্ট করা হয়েছে। আমরা [starting_index..ending_index] নির্দিষ্ট করে ব্র্যাকেটের মধ্যে একটি রেঞ্জ ব্যবহার করে স্লাইস তৈরি করি, যেখানে starting_index হল স্লাইসের প্রথম অবস্থান এবং ending_index হল স্লাইসের শেষ অবস্থানের চেয়ে এক বেশি। অভ্যন্তরীণভাবে, স্লাইস ডেটা স্ট্রাকচারটি শুরুর অবস্থান এবং স্লাইসের দৈর্ঘ্য সংরক্ষণ করে, যা ending_index বিয়োগ starting_index-এর সাথে সঙ্গতিপূর্ণ। সুতরাং, let world = &s[6..11];-এর ক্ষেত্রে, world হবে একটি স্লাইস যাতে s-এর 6 ইনডেক্সের বাইটের একটি পয়েন্টার থাকবে এবং দৈর্ঘ্য হবে 5

Figure 4-7 এটি একটি ডায়াগ্রামে দেখায়।

তিনটি টেবিল: s-এর স্ট্যাক ডেটা উপস্থাপনকারী একটি টেবিল, যা হিপের স্ট্রিং ডেটা "hello world" এর একটি টেবিলে 0 ইনডেক্সের বাইটকে নির্দেশ করে। তৃতীয় টেবিলটি স্লাইস ওয়ার্ল্ডের স্ট্যাক ডেটা পুনরায় উপস্থাপন করে, যার একটি দৈর্ঘ্যের মান 5 এবং হিপ ডেটা টেবিলের 6 বাইটকে নির্দেশ করে।

Figure 4-7: একটি String-এর অংশকে রেফারেন্স করা স্ট্রিং স্লাইস

Rust-এর .. রেঞ্জ সিনট্যাক্সের সাথে, আপনি যদি 0 ইনডেক্স থেকে শুরু করতে চান, তাহলে আপনি দুটি পিরিয়ডের আগের মানটি বাদ দিতে পারেন। অন্য কথায়, এগুলো সমান:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

একইভাবে, যদি আপনার স্লাইসটিতে String-এর শেষ বাইট অন্তর্ভুক্ত থাকে, তাহলে আপনি ট্রেইলিং সংখ্যাটি বাদ দিতে পারেন। তার মানে এগুলো সমান:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

আপনি সম্পূর্ণ স্ট্রিং-এর একটি স্লাইস নিতে উভয় মানও বাদ দিতে পারেন। তাই এগুলো সমান:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

দ্রষ্টব্য: স্ট্রিং স্লাইস রেঞ্জ ইনডেক্সগুলো অবশ্যই বৈধ UTF-8 ক্যারেক্টার সীমানায় ঘটতে হবে। আপনি যদি একটি মাল্টিবাইট ক্যারেক্টারের মাঝখানে একটি স্ট্রিং স্লাইস তৈরি করার চেষ্টা করেন, তাহলে আপনার প্রোগ্রামটি একটি এরর দিয়ে প্রস্থান করবে। স্ট্রিং স্লাইস প্রবর্তনের উদ্দেশ্যে, আমরা এই বিভাগে শুধুমাত্র ASCII অনুমান করছি; UTF-8 হ্যান্ডলিং-এর আরও বিশদ আলোচনা চ্যাপ্টার 8-এর “স্ট্রিং দিয়ে UTF-8 এনকোডেড টেক্সট সংরক্ষণ করা” বিভাগে রয়েছে।

এই সমস্ত তথ্য মাথায় রেখে, চলুন first_word পুনরায় লিখি যাতে এটি একটি স্লাইস রিটার্ন করে। "স্ট্রিং স্লাইস" বোঝায় এমন টাইপটি &str হিসাবে লেখা হয়:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

আমরা Listing 4-7-এর মতোই শব্দের শেষের ইনডেক্সটি পাই, একটি স্পেসের প্রথম ঘটনাটি সন্ধান করে। যখন আমরা একটি স্পেস খুঁজে পাই, তখন আমরা স্ট্রিং-এর শুরু এবং স্পেসের ইনডেক্সটিকে শুরুর এবং শেষের ইনডেক্স হিসাবে ব্যবহার করে একটি স্ট্রিং স্লাইস রিটার্ন করি।

এখন যখন আমরা first_word কল করি, তখন আমরা একটি একক মান ফিরে পাই যা অন্তর্নিহিত ডেটার সাথে সংযুক্ত। মানটি স্লাইসের শুরুর বিন্দুর একটি রেফারেন্স এবং স্লাইসের উপাদানগুলোর সংখ্যা নিয়ে গঠিত।

একটি second_word ফাংশনের জন্যও একটি স্লাইস রিটার্ন করা কাজ করবে:

fn second_word(s: &String) -> &str {

আমাদের কাছে এখন একটি সরল API রয়েছে যা এলোমেলো করা অনেক কঠিন, কারণ কম্পাইলার নিশ্চিত করবে যে String-এর রেফারেন্সগুলো বৈধ থাকবে। Listing 4-8-এর প্রোগ্রামের বাগটি মনে আছে, যখন আমরা প্রথম শব্দের শেষের ইনডেক্স পেয়েছিলাম কিন্তু তারপর স্ট্রিংটি পরিষ্কার করেছিলাম যাতে আমাদের ইনডেক্সটি অবৈধ হয়ে গিয়েছিল? সেই কোডটি যুক্তিগতভাবে ভুল ছিল কিন্তু কোনো তাৎক্ষণিক এরর দেখায়নি। সমস্যাগুলো পরে দেখা যেত যদি আমরা খালি স্ট্রিং দিয়ে প্রথম শব্দের ইনডেক্স ব্যবহার করতে থাকতাম। স্লাইসগুলো এই বাগটিকে অসম্ভব করে তোলে এবং আমাদের কোডে কোনো সমস্যা থাকলে তা অনেক আগেই জানিয়ে দেয়। first_word-এর স্লাইস ভার্সন ব্যবহার করলে একটি কম্পাইল-টাইম এরর হবে:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

এখানে কম্পাইলার এররটি দেওয়া হলো:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                  ------ immutable borrow later used here

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

বোরোয়িং-এর নিয়ম থেকে মনে করুন যে যদি আমাদের কাছে কোনো কিছুর ইমিউটেবল রেফারেন্স থাকে, তাহলে আমরা একটি মিউটেবল রেফারেন্সও নিতে পারি না। যেহেতু clear-এর String ছোট করা দরকার, তাই এটিকে একটি মিউটেবল রেফারেন্স নিতে হবে। clear কলের পরে println! word-এর রেফারেন্স ব্যবহার করে, তাই সেই সময়ে ইমিউটেবল রেফারেন্সটি এখনও সক্রিয় থাকতে হবে। Rust clear-এর মিউটেবল রেফারেন্স এবং word-এর ইমিউটেবল রেফারেন্সকে একই সময়ে বিদ্যমান থাকতে দেয় না এবং কম্পাইলেশন ব্যর্থ হয়। Rust শুধুমাত্র আমাদের API ব্যবহার করা সহজ করেনি, এটি কম্পাইল করার সময়ই এক শ্রেণীর এরর সম্পূর্ণরূপে দূর করেছে!

স্ট্রিং লিটারেলগুলো স্লাইস (String Literals as Slices)

মনে করে দেখুন যে আমরা স্ট্রিং লিটারেলগুলো বাইনারির ভিতরে সংরক্ষিত থাকার বিষয়ে কথা বলেছি। এখন যেহেতু আমরা স্লাইস সম্পর্কে জানি, তাই আমরা স্ট্রিং লিটারেলগুলোকে সঠিকভাবে বুঝতে পারি:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

এখানে s-এর টাইপ হল &str: এটি বাইনারির সেই নির্দিষ্ট পয়েন্টকে নির্দেশ করা একটি স্লাইস। এই কারণেই স্ট্রিং লিটারেলগুলো ইমিউটেবল; &str হল একটি ইমিউটেবল রেফারেন্স।

প্যারামিটার হিসাবে স্ট্রিং স্লাইস (String Slices as Parameters)

লিটারেল এবং String মানগুলোর স্লাইস নিতে পারা আমাদের first_word-এ আরও একটি উন্নতির দিকে নিয়ে যায়, এবং সেটি হল এর সিগনেচার:

fn first_word(s: &String) -> &str {

একজন আরও অভিজ্ঞ Rustacean পরিবর্তে Listing 4-9-এ দেখানো সিগনেচারটি লিখবেন কারণ এটি আমাদের একই ফাংশনটি &String মান এবং &str মান, উভয়ের উপর ব্যবহার করার অনুমতি দেয়।

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

যদি আমাদের কাছে একটি স্ট্রিং স্লাইস থাকে, তাহলে আমরা সেটি সরাসরি পাস করতে পারি। যদি আমাদের কাছে একটি String থাকে, তাহলে আমরা String-এর একটি স্লাইস বা String-এর একটি রেফারেন্স পাস করতে পারি। এই নমনীয়তা ডিরেফ কোয়েরশনস (deref coercions)-এর সুবিধা নেয়, একটি ফিচার যা আমরা চ্যাপ্টার ১৫-এর “ফাংশন এবং মেথড সহ ইমপ্লিসিট ডিরেফ কোয়েরশনস” বিভাগে কভার করব।

একটি String-এর রেফারেন্সের পরিবর্তে একটি স্ট্রিং স্লাইস নেওয়ার জন্য একটি ফাংশন সংজ্ঞায়িত করা আমাদের API-কে আরও সাধারণ এবং দরকারী করে তোলে, কোনো কার্যকারিতা না হারিয়ে:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

অন্যান্য স্লাইস (Other Slices)

আপনি যেমন কল্পনা করতে পারেন, স্ট্রিং স্লাইসগুলো স্ট্রিং-এর জন্য নির্দিষ্ট। কিন্তু আরও একটি সাধারণ স্লাইস টাইপও রয়েছে। এই অ্যারেটি বিবেচনা করুন:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

যেমন আমরা একটি স্ট্রিং-এর অংশের উল্লেখ করতে চাইতে পারি, তেমনি আমরা একটি অ্যারের অংশের উল্লেখ করতে চাইতে পারি। আমরা এটি এইভাবে করব:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

এই স্লাইসটির টাইপ হল &[i32]। এটি স্ট্রিং স্লাইসগুলোর মতোই কাজ করে, প্রথম উপাদানের একটি রেফারেন্স এবং একটি দৈর্ঘ্য সংরক্ষণ করে। আপনি অন্যান্য সমস্ত ধরণের কালেকশনের জন্য এই ধরনের স্লাইস ব্যবহার করবেন। আমরা চ্যাপ্টার ৮-এ ভেক্টর নিয়ে কথা বলার সময় এই কালেকশনগুলো নিয়ে বিস্তারিত আলোচনা করব।

সারসংক্ষেপ (Summary)

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

ওনারশিপ Rust-এর আরও অনেক অংশের কাজকে প্রভাবিত করে, তাই আমরা বই জুড়ে এই ধারণাগুলো নিয়ে আরও কথা বলব। চলুন চ্যাপ্টার ৫-এ যাই এবং একটি struct-এ ডেটার অংশগুলোকে একত্রিত করা দেখি।