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

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

স্লাইস (Slices) আপনাকে একটি কালেকশনের মধ্যে থাকা উপাদানগুলোর একটি অবিচ্ছিন্ন ক্রমকে (contiguous sequence) রেফারেন্স করতে দেয়। স্লাইস এক ধরনের রেফারেন্স, তাই এর কোনো মালিকানা (ownership) নেই।

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

দ্রষ্টব্য: স্ট্রিং স্লাইস পরিচিত করানোর উদ্দেশ্যে, আমরা এই বিভাগে শুধুমাত্র ASCII ধরে নিচ্ছি; অধ্যায় ৮-এর "স্ট্রিং সহ UTF-8 এনকোডেড টেক্সট সংরক্ষণ" বিভাগে UTF-8 হ্যান্ডলিং নিয়ে আরও বিস্তারিত আলোচনা করা হয়েছে।

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

fn first_word(s: &String) -> ?

first_word ফাংশনটির একটি প্যারামিটার আছে যার টাইপ &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() {}

যেহেতু আমাদের 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 মেথড ব্যবহার করে বাইট অ্যারের উপর একটি ইটারেটর (iterator) তৈরি করি:

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 একটি মেথড যা একটি কালেকশনের প্রতিটি উপাদান রিটার্ন করে এবং enumerate iter-এর ফলাফলকে মুড়িয়ে (wrap) প্রতিটি উপাদানকে একটি টাপলের অংশ হিসাবে রিটার্ন করে। enumerate থেকে রিটার্ন করা টাপলের প্রথম উপাদানটি হলো ইনডেক্স, এবং দ্বিতীয় উপাদানটি হলো উপাদানের একটি রেফারেন্স। এটি আমাদের নিজেদের ইনডেক্স গণনা করার চেয়ে একটু বেশি সুবিধাজনক।

যেহেতু enumerate মেথডটি একটি টাপল রিটার্ন করে, আমরা সেই টাপলটিকে ডিস্ট্রাকচার (destructure) করতে প্যাটার্ন ব্যবহার করতে পারি। আমরা অধ্যায় ৬-এ প্যাটার্ন নিয়ে আরও আলোচনা করব। 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 থেকে একটি পৃথক মান, তাই ভবিষ্যতে এটি বৈধ থাকবে এমন কোনো নিশ্চয়তা নেই। তালিকা ৪-৮-এর প্রোগ্রামটি বিবেচনা করুন যা তালিকা ৪-৭-এর 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 মানটি রয়েছে। আমরা s ভ্যারিয়েবলের সাথে সেই 5 মানটি ব্যবহার করে প্রথম শব্দটি বের করার চেষ্টা করতে পারতাম, কিন্তু এটি একটি বাগ হতো কারণ word-এ 5 সংরক্ষণ করার পর s-এর বিষয়বস্তু পরিবর্তিত হয়েছে।

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

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

এখন আমরা একটি শুরুর এবং একটি শেষের ইনডেক্স ট্র্যাক করছি, এবং আমাদের কাছে আরও বেশি মান রয়েছে যা একটি নির্দিষ্ট অবস্থার ডেটা থেকে গণনা করা হয়েছে কিন্তু সেই অবস্থার সাথে মোটেই আবদ্ধ নয়। আমাদের তিনটি असंबंधित ভ্যারিয়েবল রয়েছে যা সিঙ্কে রাখতে হবে।

ভাগ্যক্রমে, রাস্টের এই সমস্যার একটি সমাধান আছে: স্ট্রিং স্লাইস।

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

একটি স্ট্রিং স্লাইস (string slice) হলো একটি 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-এর ইনডেক্স ৬-এর বাইটের একটি পয়েন্টার এবং ৫ দৈর্ঘ্যের একটি মান ধারণ করবে।

চিত্র ৪-৭ এটি একটি ডায়াগ্রামে দেখাচ্ছে।

তিনটি টেবিল: s-এর স্ট্যাক ডেটা প্রতিনিধিত্বকারী একটি টেবিল, যা হীপে থাকা 'hello world' স্ট্রিং ডেটার টেবিলের ইনডেক্স ০-এর বাইটকে নির্দেশ করে। তৃতীয় টেবিলটি স্লাইস world-এর স্ট্যাক ডেটা প্রতিনিধিত্ব করে, যার দৈর্ঘ্য ৫ এবং এটি হীপ ডেটা টেবিলের ৬ নং বাইটকে নির্দেশ করে।

চিত্র ৪-৭: একটি String-এর অংশকে নির্দেশকারী স্ট্রিং স্লাইস

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

#![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 ক্যারেক্টার সীমানায় হতে হবে। আপনি যদি একটি মাল্টিবাইট ক্যারেক্টারের মাঝখানে একটি স্ট্রিং স্লাইস তৈরি করার চেষ্টা করেন, আপনার প্রোগ্রাম একটি এরর সহ বন্ধ হয়ে যাবে।

এই সমস্ত তথ্য মাথায় রেখে, আসুন 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() {}

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

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

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

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

এখন আমাদের একটি সহজবোধ্য API আছে যা ভুল করা অনেক কঠিন কারণ কম্পাইলার নিশ্চিত করবে যে String-এর রেফারেন্সগুলো বৈধ থাকবে। তালিকা ৪-৮-এর প্রোগ্রামের বাগটি মনে আছে, যখন আমরা প্রথম শব্দের শেষের ইনডেক্স পেয়েছিলাম কিন্তু তারপর স্ট্রিংটি খালি করে দিয়েছিলাম যাতে আমাদের ইনডেক্সটি অবৈধ হয়ে যায়? সেই কোডটি যৌক্তিকভাবে ভুল ছিল কিন্তু কোনো তাৎক্ষণিক এরর দেখায়নি। সমস্যাগুলো পরে দেখা যেত যদি আমরা খালি স্ট্রিংয়ের সাথে প্রথম শব্দের ইনডেক্স ব্যবহার করার চেষ্টা চালিয়ে যেতাম। স্লাইস এই বাগটিকে অসম্ভব করে তোলে এবং আমাদের কোডের সমস্যা সম্পর্কে অনেক আগে জানিয়ে দেয়। 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-এর রেফারেন্স ব্যবহার করে, তাই সেই সময়ে অপরিবর্তনীয় রেফারেন্সটি অবশ্যই সক্রিয় থাকতে হবে। রাস্ট clear-এর পরিবর্তনযোগ্য রেফারেন্স এবং word-এর অপরিবর্তনীয় রেফারেন্সকে একই সময়ে বিদ্যমান থাকতে দেয় না, এবং কম্পাইলেশন ব্যর্থ হয়। রাস্ট কেবল আমাদের API ব্যবহার করা সহজ করেনি, বরং এটি কম্পাইলের সময় একটি সম্পূর্ণ শ্রেণীর এররও দূর করেছে!

স্ট্রিং লিটারেলস (String Literals) হলো স্লাইস

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

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

s-এর টাইপ এখানে &str: এটি বাইনারির সেই নির্দিষ্ট পয়েন্টের দিকে নির্দেশকারী একটি স্লাইস। এই কারণেই স্ট্রিং লিটারেলগুলো অপরিবর্তনীয় (immutable); &str একটি অপরিবর্তনীয় রেফারেন্স।

প্যারামিটার হিসাবে স্ট্রিং স্লাইস

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

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

একজন আরও অভিজ্ঞ রাস্টেশিয়ান (Rustacean) তালিকা ৪-৯-এ দেখানো সিগনেচারটি লিখবেন কারণ এটি আমাদের &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)

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

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