ওনারশিপ কী? (What Is Ownership?)

ওনারশিপ হল নিয়মের একটি সেট যা নির্ধারণ করে কিভাবে একটি Rust প্রোগ্রাম মেমরি পরিচালনা করে। সব প্রোগ্রামকেই রান করার সময় কম্পিউটারের মেমরি ব্যবহারের পদ্ধতি পরিচালনা করতে হয়। কিছু ল্যাঙ্গুয়েজে গারবেজ কালেকশন (garbage collection) থাকে, যা প্রোগ্রাম চলার সময় নিয়মিতভাবে অব্যবহৃত মেমরি খুঁজে বের করে; অন্য ল্যাঙ্গুয়েজগুলোতে, প্রোগ্রামারকে স্পষ্টতই মেমরি বরাদ্দ (allocate) এবং মুক্ত (free) করতে হয়। Rust একটি তৃতীয় পদ্ধতি ব্যবহার করে: মেমরি ওনারশিপের একটি সিস্টেমের মাধ্যমে পরিচালিত হয়, যেখানে কম্পাইলার নিয়মের একটি সেট পরীক্ষা করে। যদি কোনো নিয়ম লঙ্ঘন করা হয়, তাহলে প্রোগ্রামটি কম্পাইল হবে না। ওনারশিপের কোনো ফিচারই আপনার প্রোগ্রাম চলার সময় এটিকে ধীর করবে না।

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

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

স্ট্যাক এবং হিপ (The Stack and the Heap)

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

স্ট্যাক এবং হিপ উভয়ই মেমরির অংশ যা রানটাইমে আপনার কোড ব্যবহার করতে পারে, তবে সেগুলো ভিন্নভাবে গঠিত। স্ট্যাক মানগুলোকে যে ক্রমে পায় সেই ক্রমে সংরক্ষণ করে এবং বিপরীত ক্রমে মানগুলো সরিয়ে দেয়। এটিকে লাস্ট ইন, ফার্স্ট আউট (last in, first out) বলা হয়। প্লেটের স্তূপের কথা চিন্তা করুন: আপনি যখন আরও প্লেট যোগ করেন, আপনি সেগুলোকে স্তূপের উপরে রাখেন এবং যখন আপনার একটি প্লেট প্রয়োজন হয়, আপনি উপরের দিক থেকে একটি প্লেট নেন। মাঝখান থেকে বা নিচ থেকে প্লেট যোগ করা বা সরানো কাজ করবে না! ডেটা যোগ করাকে স্ট্যাকের উপর পুশ করা (pushing onto the stack) বলা হয় এবং ডেটা সরিয়ে দেওয়াকে স্ট্যাক থেকে পপ করা (popping off the stack) বলা হয়। স্ট্যাকে সংরক্ষিত সমস্ত ডেটার একটি পরিচিত, নির্দিষ্ট আকার থাকতে হবে। কম্পাইল করার সময় অজানা আকারের ডেটা বা আকার পরিবর্তন হতে পারে এমন ডেটা অবশ্যই হিপে সংরক্ষণ করতে হবে।

হিপ কম সংগঠিত: আপনি যখন হিপে ডেটা রাখেন, তখন আপনি একটি নির্দিষ্ট পরিমাণ জায়গা অনুরোধ করেন। মেমরি অ্যালোকেটর (memory allocator) হিপে যথেষ্ট বড় একটি খালি জায়গা খুঁজে বের করে, এটিকে ব্যবহৃত হচ্ছে বলে চিহ্নিত করে এবং একটি পয়েন্টার (pointer) রিটার্ন করে, যেটি হল সেই অবস্থানের ঠিকানা। এই প্রক্রিয়াটিকে হিপে অ্যালোকেট করা (allocating on the heap) বলা হয় এবং কখনও কখনও এটিকে সংক্ষেপে শুধু অ্যালোকেটিং (allocating) বলা হয় (স্ট্যাকের উপর মান পুশ করা অ্যালোকেটিং হিসাবে বিবেচিত হয় না)। কারণ হিপের পয়েন্টারটি একটি পরিচিত, নির্দিষ্ট আকারের, আপনি পয়েন্টারটি স্ট্যাকে সংরক্ষণ করতে পারেন, কিন্তু যখন আপনি আসল ডেটা চান, তখন আপনাকে পয়েন্টারটি অনুসরণ করতে হবে। একটি রেস্তোরাঁয় বসার কথা চিন্তা করুন। আপনি যখন প্রবেশ করেন, তখন আপনি আপনার গ্রুপের লোকের সংখ্যা জানান এবং হোস্ট সবার জন্য উপযুক্ত একটি খালি টেবিল খুঁজে বের করে আপনাকে সেখানে নিয়ে যায়। যদি আপনার গ্রুপের কেউ দেরিতে আসে, তাহলে তারা আপনাকে খুঁজে বের করার জন্য জিজ্ঞাসা করতে পারে যে আপনাকে কোথায় বসানো হয়েছে।

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

হিপের ডেটা অ্যাক্সেস করা স্ট্যাকের ডেটা অ্যাক্সেস করার চেয়ে ধীর, কারণ আপনাকে সেখানে যাওয়ার জন্য একটি পয়েন্টার অনুসরণ করতে হবে। সমসাময়িক প্রসেসরগুলো দ্রুততর হয় যদি সেগুলো মেমরিতে কম লাফিয়ে চলে। উপমাটি চালিয়ে গেলে, একটি রেস্তোরাঁর একজন সার্ভারের কথা বিবেচনা করুন যিনি অনেক টেবিল থেকে অর্ডার নিচ্ছেন। পরবর্তী টেবিলে যাওয়ার আগে একটি টেবিলের সমস্ত অর্ডার নেওয়া সবচেয়ে কার্যকর। টেবিল A থেকে একটি অর্ডার নেওয়া, তারপর টেবিল B থেকে একটি অর্ডার, তারপর আবার A থেকে একটি, এবং তারপর আবার B থেকে একটি অর্ডার নেওয়া অনেক ধীর প্রক্রিয়া হবে। একইভাবে, একটি প্রসেসর তার কাজটি আরও ভালভাবে করতে পারে যদি এটি এমন ডেটাতে কাজ করে যা অন্যান্য ডেটার কাছাকাছি থাকে (যেমন এটি স্ট্যাকে থাকে) দূরে থাকার পরিবর্তে (যেমন এটি হিপে থাকতে পারে)।

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

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

ওনারশিপের নিয়ম (Ownership Rules)

প্রথমে, চলুন ওনারশিপের নিয়মগুলো দেখি। এই নিয়মগুলো মনে রাখুন যখন আমরা উদাহরণের মাধ্যমে কাজ করব:

  • Rust-এ প্রতিটি মানের একজন ওনার (owner) থাকে।
  • একবারে কেবল একজন ওনার থাকতে পারে।
  • যখন ওনার স্কোপের বাইরে চলে যায়, তখন মানটি ড্রপ (drop) হয়ে যাবে।

ভেরিয়েবল স্কোপ (Variable Scope)

যেহেতু আমরা এখন বেসিক Rust সিনট্যাক্স অতিক্রম করেছি, তাই আমরা উদাহরণগুলোতে সমস্ত fn main() { কোড অন্তর্ভুক্ত করব না, তাই আপনি যদি অনুসরণ করেন তবে নিম্নলিখিত উদাহরণগুলো নিজে থেকে একটি main ফাংশনের ভিতরে রাখতে ভুলবেন না। ফলস্বরূপ, আমাদের উদাহরণগুলো আরও সংক্ষিপ্ত হবে, যা আমাদের বয়লারপ্লেট কোডের পরিবর্তে আসল বিবরণে ফোকাস করতে দেবে।

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

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

s ভেরিয়েবলটি একটি স্ট্রিং লিটারেলকে নির্দেশ করে, যেখানে স্ট্রিংয়ের মানটি আমাদের প্রোগ্রামের টেক্সটে হার্ডকোড করা আছে। ভেরিয়েবলটি যে বিন্দুতে ঘোষণা করা হয়েছে সেখান থেকে বর্তমান স্কোপ শেষ হওয়া পর্যন্ত বৈধ। Listing 4-1-এ একটি প্রোগ্রাম দেখানো হলো, যেখানে s ভেরিয়েবলটি কোথায় বৈধ হবে তা টীকা দিয়ে দেখানো হয়েছে।

fn main() {
    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}

অন্য কথায়, এখানে দুটি গুরুত্বপূর্ণ সময় বিন্দু রয়েছে:

  • যখন s স্কোপের মধ্যে আসে, তখন এটি বৈধ।
  • এটি স্কোপের বাইরে যাওয়া পর্যন্ত বৈধ থাকে।

এই সময়ে, স্কোপ এবং কখন ভেরিয়েবলগুলো বৈধ তার মধ্যে সম্পর্ক অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজের মতোই। এবার আমরা String টাইপ প্রবর্তন করে এই বোধগম্যতার উপর ভিত্তি করে তৈরি করব।

String টাইপ (The String Type)

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

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

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

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

ডাবল কোলন :: অপারেটর আমাদের String টাইপের অধীনে এই বিশেষ from ফাংশনটিকে নেমস্পেস করতে দেয়, string_from-এর মতো কোনো নাম ব্যবহার করার পরিবর্তে। আমরা চ্যাপ্টার ৫-এর “মেথড সিনট্যাক্স” বিভাগে এবং চ্যাপ্টার ৭-এ “মডিউল ট্রিতে একটি আইটেমের উল্লেখ করার জন্য পাথ”-তে মডিউল সহ নেমস্পেসিং সম্পর্কে কথা বলার সময় এই সিনট্যাক্সটি নিয়ে আরও আলোচনা করব।

এই ধরনের স্ট্রিং পরিবর্তন করা যেতে পারে:

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

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{s}"); // This will print `hello, world!`
}

তাহলে, এখানে পার্থক্য কী? কেন String পরিবর্তন করা যেতে পারে কিন্তু লিটারেলগুলো পারে না? পার্থক্য হল এই দুটি টাইপ কীভাবে মেমরি নিয়ে কাজ করে।

মেমরি এবং অ্যালোকেশন (Memory and Allocation)

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

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

  • মেমরিটি অবশ্যই রানটাইমে মেমরি অ্যালোকেটর থেকে অনুরোধ করতে হবে।
  • আমাদের String-এর কাজ শেষ হয়ে গেলে এই মেমরিটি অ্যালোকেটরকে ফিরিয়ে দেওয়ার একটি উপায় প্রয়োজন।

সেই প্রথম অংশটি আমাদের দ্বারা সম্পন্ন হয়: যখন আমরা String::from কল করি, তখন এর ইমপ্লিমেন্টেশন প্রয়োজনীয় মেমরির অনুরোধ করে। এটি প্রোগ্রামিং ল্যাঙ্গুয়েজগুলোতে প্রায় সর্বজনীন।

তবে, দ্বিতীয় অংশটি ভিন্ন। গারবেজ কালেক্টর (GC) সহ ভাষাগুলোতে, GC অব্যবহৃত মেমরি ট্র্যাক করে এবং পরিষ্কার করে, এবং আমাদের এটি নিয়ে চিন্তা করতে হবে না। GC ছাড়া বেশিরভাগ ল্যাঙ্গুয়েজে, কখন মেমরি আর ব্যবহার করা হচ্ছে না তা শনাক্ত করা এবং এটিকে অনুরোধ করার মতোই স্পষ্টভাবে মুক্ত করার জন্য কোড কল করা আমাদের দায়িত্ব। এটি সঠিকভাবে করা ঐতিহাসিকভাবে একটি কঠিন প্রোগ্রামিং সমস্যা। যদি আমরা ভুলে যাই, তাহলে আমরা মেমরি নষ্ট করব। যদি আমরা এটি খুব তাড়াতাড়ি করি, তাহলে আমাদের একটি অবৈধ ভেরিয়েবল থাকবে। যদি আমরা এটি দুবার করি, সেটিও একটি বাগ। আমাদের ঠিক একটি allocate-কে ঠিক একটি free-এর সাথে যুক্ত করতে হবে।

Rust একটি ভিন্ন পথ নেয়: মেমরির মালিক ভেরিয়েবলটি স্কোপের বাইরে চলে গেলে স্বয়ংক্রিয়ভাবে মেমরি ফেরত দেওয়া হয়। এখানে স্ট্রিং লিটারেলের পরিবর্তে একটি String ব্যবহার করে Listing 4-1 থেকে আমাদের স্কোপ উদাহরণের একটি ভার্সন দেওয়া হলো:

fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid
}

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

দ্রষ্টব্য: C++-এ, একটি আইটেমের জীবনকালের শেষে রিসোর্স ডিলোকেট করার এই প্যাটার্নটিকে কখনও কখনও রিসোর্স অ্যাকুইজিশন ইজ ইনিশিয়ালাইজেশন (Resource Acquisition Is Initialization - RAII) বলা হয়। আপনি যদি RAII প্যাটার্ন ব্যবহার করে থাকেন তবে Rust-এর drop ফাংশনটি আপনার কাছে পরিচিত হবে।

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

ভেরিয়েবল এবং ডেটার মধ্যে মিথস্ক্রিয়া: মুভ (Variables and Data Interacting with Move)

Rust-এ একাধিক ভেরিয়েবল একই ডেটার সাথে ভিন্নভাবে ইন্টারঅ্যাক্ট করতে পারে। চলুন Listing 4-2-এর একটি ইন্টিজার ব্যবহার করে একটি উদাহরণ দেখি।

fn main() {
    let x = 5;
    let y = x;
}

আমরা সম্ভবত অনুমান করতে পারি এটি কী করছে: "x-এ 5 মান বাইন্ড করো; তারপর x-এর মানের একটি কপি তৈরি করো এবং এটিকে y-এ বাইন্ড করো।" আমাদের এখন দুটি ভেরিয়েবল আছে, x এবং y, এবং উভয়ই 5-এর সমান। உண்மையில் এটিই ঘটছে, কারণ ইন্টিজারগুলো হল পরিচিত, নির্দিষ্ট আকারের সহজ মান এবং এই দুটি 5 মান স্ট্যাকের উপর পুশ করা হয়।

এবার চলুন String ভার্সনটি দেখি:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

এটি দেখতে অনেকটা একই রকম, তাই আমরা অনুমান করতে পারি যে এটি একইভাবে কাজ করবে: অর্থাৎ, দ্বিতীয় লাইনটি s1-এর মানের একটি কপি তৈরি করবে এবং এটিকে s2-তে বাইন্ড করবে। কিন্তু আসলে এটি ঘটে না।

String-এর ক্ষেত্রে পর্দার আড়ালে কী ঘটছে তা দেখতে Figure 4-1 দেখুন। একটি String তিনটি অংশ নিয়ে গঠিত, যা বাম দিকে দেখানো হয়েছে: স্ট্রিংয়ের কনটেন্ট ধারণকারী মেমরির একটি পয়েন্টার, একটি দৈর্ঘ্য (length) এবং একটি ধারণক্ষমতা (capacity)। ডেটার এই গ্রুপটি স্ট্যাকে সংরক্ষণ করা হয়। ডানদিকে হিপের মেমরি রয়েছে যা কনটেন্ট ধারণ করে।

দুটি টেবিল: প্রথম টেবিলটি স্ট্যাকের উপর s1-এর উপস্থাপনা ধারণ করে, যার মধ্যে রয়েছে এর দৈর্ঘ্য (5), ধারণক্ষমতা (5) এবং দ্বিতীয় টেবিলের প্রথম মানের একটি পয়েন্টার। দ্বিতীয় টেবিলটি হিপের উপর স্ট্রিং ডেটার উপস্থাপনা ধারণ করে, বাইট বাই বাইট।

Figure 4-1: "hello" মান ধারণকারী একটি String-এর মেমরি উপস্থাপনা, যা s1-এর সাথে বাইন্ড করা

দৈর্ঘ্য হল String-এর কনটেন্টগুলো বর্তমানে কত বাইট মেমরি ব্যবহার করছে। ধারণক্ষমতা হল মোট মেমরির পরিমাণ, বাইটে, যা String অ্যালোকেটর থেকে পেয়েছে। দৈর্ঘ্য এবং ধারণক্ষমতার মধ্যে পার্থক্য গুরুত্বপূর্ণ, কিন্তু এই প্রসঙ্গে নয়, তাই আপাতত, ধারণক্ষমতা উপেক্ষা করা যেতে পারে।

যখন আমরা s1-কে s2-তে অ্যাসাইন করি, তখন String ডেটা কপি করা হয়, অর্থাৎ আমরা পয়েন্টার, দৈর্ঘ্য এবং ধারণক্ষমতা কপি করি যা স্ট্যাকের উপর রয়েছে। আমরা হিপের ডেটা কপি করি না যেখানে পয়েন্টারটি নির্দেশ করে। অন্য কথায়, মেমরিতে ডেটার উপস্থাপনাটি Figure 4-2-এর মতো দেখায়।

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

Figure 4-2: ভেরিয়েবল s2-এর মেমরি উপস্থাপনা, যেখানে s1-এর পয়েন্টার, দৈর্ঘ্য এবং ধারণক্ষমতার একটি কপি রয়েছে

উপস্থাপনাটি Figure 4-3-এর মতো নয়, যেটি মেমরি দেখতে কেমন হত যদি Rust হিপ ডেটাও কপি করত। যদি Rust এটি করত, তাহলে হিপের ডেটা বড় হলে s2 = s1 অপারেশনটি রানটাইম পারফরম্যান্সের ক্ষেত্রে খুব ব্যয়বহুল হতে পারত।

চারটি টেবিল: দুটি টেবিল s1 এবং s2-এর জন্য স্ট্যাক ডেটা উপস্থাপন করে এবং প্রতিটি হিপে স্ট্রিং ডেটার নিজস্ব কপির দিকে নির্দেশ করে।

Figure 4-3: s2 = s1 কী করতে পারে তার আরেকটি সম্ভাবনা, যদি Rust হিপ ডেটাও কপি করত

আগে, আমরা বলেছিলাম যে যখন একটি ভেরিয়েবল স্কোপের বাইরে চলে যায়, তখন Rust স্বয়ংক্রিয়ভাবে drop ফাংশনটিকে কল করে এবং সেই ভেরিয়েবলের জন্য হিপ মেমরি পরিষ্কার করে। কিন্তু Figure 4-2-তে দেখানো হয়েছে যে উভয় ডেটা পয়েন্টার একই অবস্থানের দিকে নির্দেশ করছে। এটি একটি সমস্যা: যখন s2 এবং s1 স্কোপের বাইরে চলে যায়, তখন তারা উভয়ই একই মেমরি মুক্ত করার চেষ্টা করবে। এটি ডাবল ফ্রি (double free) এরর হিসাবে পরিচিত এবং এটি মেমরি নিরাপত্তার বাগগুলোর মধ্যে একটি যা আমরা আগে উল্লেখ করেছি। দুবার মেমরি মুক্ত করলে মেমরি ক্ষতিগ্রস্ত হতে পারে, যা সম্ভাব্য নিরাপত্তা দুর্বলতার দিকে পরিচালিত করতে পারে।

মেমরির নিরাপত্তা নিশ্চিত করতে, let s2 = s1; লাইনের পরে, Rust s1-কে আর বৈধ বলে মনে করে না। অতএব, s1 যখন স্কোপের বাইরে চলে যায় তখন Rust-কে কিছু মুক্ত করতে হবে না। s2 তৈরি হওয়ার পরে আপনি যখন s1 ব্যবহার করার চেষ্টা করবেন তখন কী ঘটে তা দেখুন; এটি কাজ করবে না:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{s1}, world!");
}

আপনি এইরকম একটি এরর পাবেন কারণ Rust আপনাকে অবৈধ রেফারেন্স ব্যবহার করা থেকে বিরত রাখে:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:15
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{s1}, world!");
  |               ^^^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

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

আপনি যদি অন্য ল্যাঙ্গুয়েজের সাথে কাজ করার সময় শ্যালো কপি (shallow copy) এবং ডিপ কপি (deep copy) শব্দগুলো শুনে থাকেন, তাহলে ডেটা কপি না করে পয়েন্টার, দৈর্ঘ্য এবং ধারণক্ষমতা কপি করার ধারণাটি সম্ভবত শ্যালো কপি করার মতো শোনাবে। কিন্তু যেহেতু Rust প্রথম ভেরিয়েবলটিকেও অকার্যকর করে, তাই এটিকে শ্যালো কপি বলার পরিবর্তে, এটি মুভ (move) নামে পরিচিত। এই উদাহরণে, আমরা বলব যে s1 s2-তে মুভ করা হয়েছে। সুতরাং, আসলে যা ঘটে তা Figure 4-4-এ দেখানো হয়েছে।

তিনটি টেবিল: s1 এবং s2 টেবিলগুলো স্ট্যাকের উপর সেই স্ট্রিংগুলোকে উপস্থাপন করে, যথাক্রমে, এবং উভয়ই হিপের একই স্ট্রিং ডেটার দিকে নির্দেশ করে।
s1 টেবিলটি ধূসর করা হয়েছে কারণ s1 আর বৈধ নয়; শুধুমাত্র s2 হিপ ডেটা অ্যাক্সেস করতে ব্যবহার করা যেতে পারে।

Figure 4-4: s1 অকার্যকর হওয়ার পরে মেমরিতে উপস্থাপনা

এটি আমাদের সমস্যার সমাধান করে! শুধুমাত্র s2 বৈধ থাকায়, যখন এটি স্কোপের বাইরে চলে যাবে তখন এটি একাই মেমরি মুক্ত করবে এবং আমরা সম্পন্ন করব।

উপরন্তু, এর দ্বারা বোঝানো একটি ডিজাইন পছন্দ রয়েছে: Rust স্বয়ংক্রিয়ভাবে আপনার ডেটার “ডিপ” কপি তৈরি করবে না। অতএব, যেকোনো স্বয়ংক্রিয় কপিং রানটাইম পারফরম্যান্সের ক্ষেত্রে সস্তা বলে ধরে নেওয়া যেতে পারে।

স্কোপ এবং অ্যাসাইনমেন্ট (Scope and Assignment)

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

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

    println!("{s}, world!");
}

আমরা প্রাথমিকভাবে একটি ভেরিয়েবল s ঘোষণা করি এবং "hello" মান সহ একটি String-এর সাথে আবদ্ধ করি। তারপর আমরা "ahoy" মান সহ একটি নতুন String তৈরি করি এবং s-এ বরাদ্দ করি। এই সময়ে, হিপের মূল মানটিকে কিছুই নির্দেশ করছে না।

একটি টেবিল s স্ট্রিং মানটিকে স্ট্যাকের উপর উপস্থাপন করে, হিপের স্ট্রিং ডেটার দ্বিতীয় অংশ (ahoy) নির্দেশ করে, মূল স্ট্রিং ডেটা (hello) ধূসর হয়ে গেছে কারণ এটি আর অ্যাক্সেস করা যাবে না।

Figure 4-5: মূল মানটি সম্পূর্ণরূপে প্রতিস্থাপিত হওয়ার পরে মেমরিতে উপস্থাপনা।

মূল স্ট্রিংটি অবিলম্বে স্কোপের বাইরে চলে যায়। Rust এটির উপর drop ফাংশন চালাবে এবং এর মেমরি অবিলম্বে মুক্ত করা হবে। যখন আমরা শেষে মানটি প্রিন্ট করি, তখন এটি "ahoy, world!" হবে।

ভেরিয়েবল এবং ডেটার মধ্যে মিথস্ক্রিয়া: ক্লোন (Variables and Data Interacting with Clone)

আমরা যদি String-এর হিপ ডেটার ডিপ কপি করতে চাই, শুধু স্ট্যাক ডেটা নয়, তাহলে আমরা clone নামক একটি সাধারণ মেথড ব্যবহার করতে পারি। আমরা চ্যাপ্টার ৫-এ মেথড সিনট্যাক্স নিয়ে আলোচনা করব, কিন্তু যেহেতু মেথডগুলো অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজের একটি সাধারণ ফিচার, তাই আপনি সম্ভবত সেগুলো আগে দেখেছেন।

এখানে clone মেথডের একটি উদাহরণ দেওয়া হল:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {s1}, s2 = {s2}");
}

এটি ঠিকঠাক কাজ করে এবং স্পষ্টতই Figure 4-3-তে দেখানো আচরণ তৈরি করে, যেখানে হিপ ডেটা কপি করা হয়

আপনি যখন clone-এর একটি কল দেখেন, তখন আপনি জানেন যে কিছু নির্বিচার কোড এক্সিকিউট করা হচ্ছে এবং সেই কোডটি ব্যয়বহুল হতে পারে। এটি একটি ভিজ্যুয়াল ইন্ডিকেটর যে ভিন্ন কিছু ঘটছে।

শুধুমাত্র স্ট্যাক-ডেটা: কপি (Stack-Only Data: Copy)

আরেকটি বিষয় রয়েছে যা নিয়ে আমরা এখনও কথা বলিনি। ইন্টিজার ব্যবহার করে এই কোডটি—যার অংশ Listing 4-2-তে দেখানো হয়েছিল—কাজ করে এবং বৈধ:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {x}, y = {y}");
}

কিন্তু এই কোডটি আমরা যা শিখেছি তার বিপরীত বলে মনে হচ্ছে: আমাদের কাছে clone-এর কোনো কল নেই, তবুও x বৈধ এবং y-তে সরানো হয়নি।

কারণ হল, ইন্টিজারের মতো টাইপগুলোর কম্পাইল করার সময় একটি পরিচিত আকার থাকে, সেগুলো সম্পূর্ণরূপে স্ট্যাকে সংরক্ষণ করা হয়, তাই আসল মানগুলোর কপি তৈরি করা দ্রুত। তার মানে আমরা y ভেরিয়েবল তৈরি করার পরে x-কে অবৈধ করতে চাইব এমন কোনো কারণ নেই। অন্য কথায়, এখানে ডিপ এবং শ্যালো কপি করার মধ্যে কোনো পার্থক্য নেই, তাই clone কল করা வழக்கী শ্যালো কপি করার থেকে আলাদা কিছু করবে না এবং আমরা এটিকে বাদ দিতে পারি।

Rust-এর একটি বিশেষ অ্যানোটেশন রয়েছে যাকে Copy ট্রেইট বলা হয়, যা আমরা ইন্টিজারের মতো স্ট্যাকে সংরক্ষিত টাইপগুলোতে রাখতে পারি (আমরা চ্যাপ্টার 10-এ ট্রেইট সম্পর্কে আরও কথা বলব)। যদি একটি টাইপ Copy ট্রেইট ইমপ্লিমেন্ট করে, তাহলে সেটি ব্যবহার করা ভেরিয়েবলগুলো মুভ করে না, বরং তুচ্ছভাবে কপি করা হয়, অন্য ভেরিয়েবলে অ্যাসাইন করার পরেও সেগুলো বৈধ থাকে।

যদি টাইপ বা এর কোনো অংশ Drop ট্রেইট ইমপ্লিমেন্ট করে, তাহলে Rust আমাদের Copy দিয়ে একটি টাইপ অ্যানোটেট করতে দেবে না। যদি টাইপটির মান স্কোপের বাইরে চলে গেলে কিছু বিশেষ ঘটার প্রয়োজন হয় এবং আমরা সেই টাইপে Copy অ্যানোটেশন যোগ করি, তাহলে আমরা একটি কম্পাইল-টাইম এরর পাব। ট্রেইট ইমপ্লিমেন্ট করার জন্য আপনার টাইপে কীভাবে Copy অ্যানোটেশন যোগ করবেন সে সম্পর্কে জানতে, Appendix C-এর “ডেরিভেবল ট্রেইটস” দেখুন।

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

  • সমস্ত ইন্টিজার টাইপ, যেমন u32
  • বুলিয়ান টাইপ, bool, যার মান true এবং false
  • সমস্ত ফ্লোটিং-পয়েন্ট টাইপ, যেমন f64
  • ক্যারেক্টার টাইপ, char
  • টাপল, যদি সেগুলোতে শুধুমাত্র এমন টাইপ থাকে যেগুলো Copy ইমপ্লিমেন্ট করে। উদাহরণস্বরূপ, (i32, i32) Copy ইমপ্লিমেন্ট করে, কিন্তু (i32, String) করে না।

ওনারশিপ এবং ফাংশন (Ownership and Functions)

একটি ফাংশনে একটি মান পাস করার মেকানিক্স একটি ভেরিয়েবলে একটি মান অ্যাসাইন করার মতোই। একটি ফাংশনে একটি ভেরিয়েবল পাস করা অ্যাসাইনমেন্টের মতোই মুভ বা কপি করবে। Listing 4-3-তে কিছু টীকা সহ একটি উদাহরণ রয়েছে যা দেখায় যে কোথায় ভেরিয়েবলগুলো স্কোপের মধ্যে যায় এবং বাইরে যায়।

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // because i32 implements the Copy trait,
                                    // x does NOT move into the function,
    println!("{}", x);              // so it's okay to use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.

আমরা যদি takes_ownership কলে s ব্যবহার করার চেষ্টা করতাম, তাহলে Rust একটি কম্পাইল-টাইম এরর দিত। এই স্ট্যাটিক চেকগুলো আমাদের ভুল থেকে রক্ষা করে। main-এ কোড যোগ করার চেষ্টা করুন যা s এবং x ব্যবহার করে, এটা দেখার জন্য যে আপনি কোথায় সেগুলো ব্যবহার করতে পারেন এবং কোথায় ওনারশিপের নিয়মগুলো আপনাকে তা করতে বাধা দেয়।

রিটার্ন ভ্যালু এবং স্কোপ (Return Values and Scope)

মান রিটার্ন করাও ওনারশিপ স্থানান্তর করতে পারে। Listing 4-4 Listing 4-3-এর মতো একই টীকা সহ কিছু মান রিটার্ন করে এমন একটি ফাংশনের উদাহরণ দেখায়।

fn main() {
    let s1 = gives_ownership();        // gives_ownership moves its return
                                       // value into s1

    let s2 = String::from("hello");    // s2 comes into scope

    let s3 = takes_and_gives_back(s2); // s2 is moved into
                                       // takes_and_gives_back, which also
                                       // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {       // gives_ownership will move its
                                       // return value into the function
                                       // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                        // some_string is returned and
                                       // moves out to the calling
                                       // function
}

// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
    // a_string comes into
    // scope

    a_string  // a_string is returned and moves out to the calling function
}

একটি ভেরিয়েবলের ওনারশিপ প্রতিবার একই প্যাটার্ন অনুসরণ করে: অন্য ভেরিয়েবলে একটি মান অ্যাসাইন করা এটিকে সরিয়ে দেয়। যখন একটি ভেরিয়েবল যাতে হিপের ডেটা রয়েছে, স্কোপের বাইরে চলে যায়, তখন drop দ্বারা মানটি পরিষ্কার করা হবে, যদি না ডেটার ওনারশিপ অন্য কোনো ভেরিয়েবলে সরানো হয়।

যদিও এটি কাজ করে, ওনারশিপ নেওয়া এবং তারপর প্রতিটি ফাংশনের সাথে ওনারশিপ ফিরিয়ে দেওয়া একটু ক্লান্তিকর। আমরা যদি একটি ফাংশনকে একটি মান ব্যবহার করতে দিতে চাই কিন্তু ওনারশিপ নিতে না চাই? এটি বেশ বিরক্তিকর যে আমরা যা কিছু পাস করি তাও ফিরিয়ে দিতে হবে যদি আমরা এটি আবার ব্যবহার করতে চাই, সেইসাথে ফাংশনের বডি থেকে প্রাপ্ত ডেটা যা আমরা রিটার্ন করতে চাইতে পারি।

Rust আমাদের একটি টাপল ব্যবহার করে একাধিক মান রিটার্ন করতে দেয়, যেমনটি Listing 4-5-এ দেখানো হয়েছে।

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

    let (s2, len) = calculate_length(s1);

    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

কিন্তু এটি খুব বেশি আনুষ্ঠানিকতা এবং এমন একটি ধারণার জন্য অনেক কাজ যা সাধারণ হওয়া উচিত। আমাদের সৌভাগ্য যে, Rust-এর ওনারশিপ স্থানান্তর না করে একটি মান ব্যবহার করার জন্য একটি ফিচার রয়েছে, যাকে রেফারেন্স (references) বলা হয়।