পাঠ ৪.১

Ownership কী?

What Is Ownership?

Ownership Rust-এর সবচেয়ে নিজস্ব এবং সবচেয়ে গুরুত্বপূর্ণ feature। এটা এমন একটা rule-set, যেটা নির্ধারণ করে program-এর memory কখন allocate হবে আর কখন free হবে — এবং compiler এই rule-গুলো compile time-এ enforce করে। Rule break হলে program compile-ই হবে না। Runtime overhead নেই — সব কাজ compile time-এ।

অন্য language-এর সাথে compare করলে — JavaScript, Python, Go-তে garbage collector memory free করে; C, C++-এ programmer-কেই hand-এ allocate এবং free করতে হয়। Rust-এর approach একটা তৃতীয় পথ: ownership system, যেটা GC ছাড়াই memory safety দেয়।

Stack এবং Heap

Ownership বুঝতে হলে আগে stack ও heap বুঝতে হবে — Rust এই দুই memory area-কে আলাদাভাবে manage করে।

Stack — দ্রুত, নিয়মিত

Stack data add করে এবং remove করে LIFO (Last-In-First-Out) order-এ — থালার stack-এর মতো, উপর থেকে নাও, উপরে রাখো। Add করাকে বলে push, remove করাকে pop

Stack-এ data রাখতে হলে compile time-এ size জানা থাকতে হবে। Push করা fast — allocator কোথায় রাখবে সেটা খুঁজতে হয় না, top-এই বসায়।

Heap — flexible, slow

যেসব data-এর size compile-time-এ অজানা, বা যেগুলো পরে বাড়তে/কমতে পারে — সেগুলো heap-এ থাকে। Heap-এ allocate করতে allocator যথেষ্ট জায়গা খুঁজে, mark করে, এবং একটা pointer (address) return করে। সেই pointer (যার size fixed) stack-এ store করা যায়।

Heap-এ allocate করা stack-এ push-এর চেয়ে slow। Access-ও slow, কারণ pointer follow করে heap-এ যেতে হয়। Modern processor cache-এ কাছাকাছি data থাকলে fast — তাই memory-তে ছিটানো access slow।

Restaurant analogy: তুমি ৪ জন নিয়ে restaurant-এ ঢুকলে — host সবার বসার মতো একটা table খুঁজে দেখায়। কেউ দেরি করে এলে host-কে জিজ্ঞেস করে কোথায় বসেছ। allocator-ও এভাবে কাজ করে।

Function call হলে — argument-গুলো (যেগুলোর মধ্যে heap-data-এর pointer-ও থাকতে পারে) এবং local variable-গুলো stack-এ push হয়। Function শেষ হলে সেগুলো pop হয়।

Ownership-এর তিনটি rule

এই তিনটি rule মাথায় রেখে নিচের example-গুলো দেখো:

  1. Rust-এ প্রতিটি value-এর একটি owner থাকে।
  2. একই সময়ে কেবল একজন owner-ই থাকতে পারে।
  3. Owner scope-এর বাইরে চলে গেলে value drop হয় (মানে memory free হয়)।

Variable scope

Scope হলো যেই block-এর মধ্যে variable valid:

fn main() {
    {                      // s is not valid here, since 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 declare হলে valid হয়; scope-এর শেষে invalid হয়।

String type

Ownership-এর rule দেখাতে integer-এর মতো simple type যথেষ্ট না। String use করব — heap-এ থাকা একটা complex type।

আগেও আমরা string literal দেখেছি: "hello"। কিন্তু literal hardcoded — runtime-এ change করা যায় না, এবং compile-time-এ size জানা। User-input বা runtime-এ generate হওয়া text-এর জন্য এটা যথেষ্ট না।

এর বিকল্প — String type। Heap-এ data রাখে, size unknown বা changeable হতে পারে:

let s = String::from("hello");

:: দিয়ে namespace করা — এই from function-টা String type-এর।

String mutate করা যায়:

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!`
}

কিন্তু literal mutate করা যায় না। তফাৎটা memory-management-এ — সেটাই এই অধ্যায়ের মূল বিষয়।

Memory এবং allocation

String literal-এর content compile-time-এ জানা — তাই সরাসরি executable-এ hardcoded হয়। দ্রুত, efficient। কিন্তু runtime-এ size বদলায় বা unknown — এমন data এভাবে রাখা যাবে না।

String-এর জন্য — runtime-এ heap-এ memory allocate করতে হয়। দুটো জিনিস জরুরি:

  1. Runtime-এ allocator-এর কাছ থেকে memory request করা।
  2. কাজ শেষ হলে সেটা allocator-কে ফেরত দেওয়া।

প্রথমটা সব language-ই করে — String::from ভেতরে allocate করছে।

দ্বিতীয়টায় language-গুলো ভিন্ন:

  • GC-যুক্ত language (Java, Go, JS): GC unused memory track করে, automatic free করে।
  • GC ছাড়া language (C, C++): programmer-কে নিজে free করতে হয়। ভুললে memory leak; খুব আগে free করলে invalid pointer; দু'বার free করলে undefined behavior।

Rust-এর পথ: যে variable value-টার মালিক, সে scope-এর বাইরে গেলে memory automatic ফেরত যায়। উদাহরণ:

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
}

Scope-এর শেষে Rust automatic একটা special function call করে — dropString-এর author এই drop-এ memory release-এর কোড লিখেছেন। Closing curly bracket-এ এটা automatic চলে।

C++-এ এটাকে RAII (Resource Acquisition Is Initialization) বলে। Rust-এর approach সেটারই variant — কিন্তু compiler-এর enforcement-এর সাথে।

Variable এবং data interact — Move

Integer — copy

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

এখানে x-এর value ৫, y-এর-ও ৫। দুটো আলাদা variable, দু'জনই stack-এ। Integer-এর size known এবং fixed, তাই copy quick।

String — কী হয়?

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

দেখতে integer-এর মতোই — কিন্তু আসলে অন্য কিছু হয়। বুঝতে গেলে আগে দেখি String ভিতরে কী।

একটা String তিনটি জিনিসের সমষ্টি (stack-এ রাখা):

  • একটা pointer — heap-এ যেখানে আসল content আছে।
  • length — content কত byte ব্যবহার করছে।
  • capacity — heap-এ allocate-করা মোট জায়গা।

আর হিপ-এ থাকে আসল characters: h, e, l, l, o।

let s2 = s1; করলে — stack-এর তিনটি জিনিস (pointer, length, capacity) copy হয়। কিন্তু heap-এর data copy হয় না। অর্থাৎ, এখন s1 এবং s2 দু'জনেই একই heap memory-তে point করছে।

এটা যদি pure shallow copy হতো — সমস্যা: scope শেষ হলে দু'জনই একই memory free করতে চাইবে। এটাকে বলে double free error — memory corruption, security risk।

Rust-এর সমাধান: let s2 = s1;-এর পর s1-কে invalidate করে দেয় — এখন s1 ব্যবহার করা যাবে না। শুধু s2-ই scope শেষে drop হবে।

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

    println!("{s1}, world!");
}
compile errortext
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:16
  |
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

Compiler বলছে — s1 "moved" হয়ে গেছে। Shallow copy-এর সাথে invalidate-এর combination — Rust-এ এটাকে move বলে। বলা হয়, "s1 was moved into s2"

Rust কখনো automatic deep copy করে না — এই design-এ কোনো হিডেন cost নেই। Automatic যা হয়, সব cheap।

নতুন value assign — পুরোনোটা drop

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

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

নতুন String s-এ assign হলে পুরোনো "hello"-এর দিকে আর কেউ point করছে না — Rust সেই মুহূর্তেই drop করে দেয়, scope-এর শেষ পর্যন্ত অপেক্ষা করে না।

Clone — explicit deep copy

Heap-data-ও copy করতে চাইলে — .clone() method use করো:

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

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

এখানে heap-এ আসল data-ও copy হয়েছে — দুটো আলাদা allocation। s1 এখনো valid। কিন্তু clone() দেখলেই বুঝবে — কিছু expensive কাজ হচ্ছে। এটা একটা visual indicator।

Stack-only data — Copy trait

Integer-এর সাথে ফিরে আসি:

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

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

এখানে clone() ছাড়াই x valid রয়ে গেল। কারণ — integer পুরোপুরি stack-এ থাকে, এর deep/shallow copy বলে আলাদা কিছু নেই, copy trivial।

Rust-এ এই behavior-এর জন্য একটা Copy trait আছে। Type যদি Copy implement করে, assignment-এ move না হয়ে copy হবে। মূল variable valid থাকে।

কোন type Copy implement করে?

  • সব integer type — u32, ইত্যাদি।
  • booltrue, false
  • সব floating-point type — f64, ইত্যাদি।
  • char
  • Tuple — যদি ভিতরের সব type Copy হয়। যেমন (i32, i32) Copy, কিন্তু (i32, String) না।

Rule of thumb: যেসব type allocation-এর প্রয়োজন হয় না বা কোনো resource না, তারাই Copy।

(Note: Type-এ Drop implement থাকলে Copy implement করা যাবে না।)

Function call-এ ownership

Variable-কে function-এ pass করা — assignment-এর মতোই। Move হবে অথবা copy হবে।

src/main.rsrust
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,
                                    // so it's okay to use x afterward.

} // Here, x goes out of scope, then s. However, 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) call-এর পর s ব্যবহার করতে চাইলে compile-error। x Copy, তাই makes_copy(x)-এর পরও use করা যাবে।

Return value — ownership transfer

Function থেকে return করলেও ownership transfer হয়:

src/main.rsrust
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 {
    let some_string = String::from("yours");
    some_string
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string
}

এর সমস্যা

লক্ষ্য করো — কোনো function-কে value use করিয়ে আবার ব্যবহার করতে চাইলে সবসময় ownership ফেরত pass করতে হচ্ছে। Tedious! Tuple দিয়ে length-ও একসাথে return করা যায়:

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)
}

কিন্তু এটা একটা common pattern-এর জন্য অনেক ceremony। এর সমাধান — reference। পরের পাঠে দেখব।

এই পাঠ থেকে যা শিখলে

  • Stack — fixed-size, LIFO, fast। Heap — flexible-size, pointer থাকে, slower।
  • Ownership-এর তিন rule — প্রতিটা value-র একজন owner; একজনই owner; owner scope-এ না থাকলে value drop।
  • String heap-এ থাকে; pointer + length + capacity stack-এ।
  • let s2 = s1; — String move করে; s1 invalid। Integer copy হয় (Copy trait)।
  • .clone() দিয়ে explicit deep copy (expensive)।
  • Function call ও return-ও ownership move/copy করে — পরের পাঠে reference দিয়ে এর সমাধান।